-
Notifications
You must be signed in to change notification settings - Fork 952
Grails 2.3: REST Improvements
Details on the upcoming improvements to REST support for Grails 2.3
!!THIS DOCUMENT IS A DRAFT / WIP AND IS NOT FULLY FLESHED OUT YET!!
See JIRA issue GRAILS-9888 for related issues and features as part of this proposal
Grails currently ships with no built in web services client library. This means users often get lost trying out one of the available plugins or try to use the abomination that is Groovy's WSClient library.
Grails needs to ship with both a lower level REST client and a higher level client that allows unmarshalling of JSON or XML responses (conversion to domain or POGO instances).
The proposal for the low level client is to port and enhance the rest-client-builder plugin into Grails data, integrating it with the new Async programming APIs for Grails.
The higher level client, which will use the low-level client internally, will build on the Grails Data APIs and provide a GORM implementation for REST
class Book {
String title
static mapWith = "REST"
static mapping = {
endpoint "/books"
format "xml"
}
}
Instances can be retrieved an unmarshalled with the regular get
method (which will issue a GET request):
def b = Book.get(1) // GET /books
Or asynchronously:
Book.async.get(1).onComplete { book ->
...
}
Creating a new resource can be done with save
(which will issue POST with the marshalled object):
new Book(title:"The Stand").save() // POST /books
Updating a resource can be done with 'save' on an existing resource (which will issue a PUT with the marshalled object):
def b = Book.get(1) // GET /books/1
b.save() // PUT /books/1
Basic GORM queries are supported with query parameters:
def b = Book.findByTitle("The Stand") // GET /books?title=The%20Stand
For more advanced queries, one proposal is to support MongoDB's JSON query format (TBD).
Marshalling and unmarshalling is done automatically unless a marshaller or unmarshaller is specified:
static mapping = {
marshaller { obj, xml ->
xml.book(title:obj.title)
}
unmarshaller { obj, xml ->
obj.title = xml.@title.text()
}
}
The marshaller
and unmarshaller
can either be a closure or a class that defines marshall
and unmarshall
methods.
Current versions of Grails feature some REST support, but it has limitations and requires more work than necessary. It is also not integrated within any client-side support and the integration with URL mappings is weak.
URL mappings need to be extended to be aware of the HTTP method, and the current 'resource' linking mechanism extended to allow nesting of resources and representation of both single (no id) and multiple resources.
A URL mapping using resource
:
"/book"(resource:'book')
Will create URL mappings like:
URL | Grails Action |
---|---|
GET /book/create | create |
POST /book | save |
GET /book | show |
GET /book/edit | edit |
PUT /book | update |
DELETE /book | delete |
A URL mapping using resources
instead of resource
"/books"(resources:'book')
Will create URL mappings like:
URL | Grails Action |
---|---|
GET /books | index |
GET /books/create | create |
POST /books | save |
GET /books/1 | show |
GET /books/1/edit | edit |
PUT /books/1 | update |
DELETE /books/1 | delete |
A mapping like this:
"/books"(resources:'book') {
"/authors"(resources:"author")
}
Will result in URLs like:
URL | Grails Action |
---|---|
GET /books/1/authors | index |
GET /books/1/authors/create | create |
POST /books/1/authors | save |
GET /books/1/authors/1 | show |
GET /books/1/authors/edit/1 | edit |
PUT /books/1/authors/1 | update |
DELETE /books/1/authors/1 | delete |
With single resources:
"/book"(resource:'book') {
"/authors"(resources:"author")
}
Will be:
URL | Grails Action |
---|---|
GET /book/authors | index |
GET /book/authors/create | create |
POST /book/authors | save |
GET /book/authors/1 | show |
GET /book/authors/edit/1 | edit |
PUT /book/authors/1 | update |
DELETE /book/authors/1 | delete |
The current implementation of <g:link>
does not understand reverse generation links based on HTTP method. The link generation of Grails is to be enhanced to allow the specification of the HTTP method and in accordance generate the appropriate link (including hidden http method override):
<g:link resource="book">...</g:link> -> /books
<g:link resource="book" action="create">...</g:link> -> /books/create
<g:form resource="book" method="POST"> -> /books
<g:link resource="${book}"> -> /books/1
<g:link resource="book" id="1"> -> /books/1
<g:link resource="book" action="edit" id="1"> -> /books/edit/1
<g:form resource="book" id="1" method="PUT"> -> /books/1
<g:form resource="${book}" method="PUT"> -> /books/1
<g:form resource="${book}" method="DELETE"> -> /books/1
<g:form resource="book" id="1" method="DELETE"> -> /books/1
// nested resources
<g:form resource="book/author" bookId="1" id="2" method="DELETE"> -> /books/1/authors/2
<g:form resource="book/author" bookId="1" id="2" method="PUT"> -> /books/1/authors/2
<g:link resource="book/author" action="edit" bookId="1" id="2"> -> /books/1/authors/edit/1
etc.
A new respond
method will be added that simplifies handling responses for RESTful controller actions. The following code
class BookController {
def show(Long id) {
respond Book.get(id), formats:['xml', 'json', 'html']
}
}
Would be equivalent to this:
class BookController {
def show(Long id) {
def b = Book.get(id)
if(b) {
withFormat {
xml {
render b as XML
}
json {
render b as JSON
}
html book: b
}
}
else {
render status:404
}
}
}
It will also be possible to implement this declaratively:
class BookController {
static respondWith = [show:['json', 'xml', 'html']]
def show(Long id) {
respond Book.get(id)
}
}
Command/Domain object binding will be updated to support binding from JSON and XML packets (in addition to the existing params support) if the request matches a RESTful pattern and the content type is appropriate. Creating and Updating will be possible with:
class BookController {
static respondsWith = [update:['json', 'xml', 'html']]
def update(Book updatedBook) {
Book.withTransaction {
updatedBook.save()
respond updatedBook
}
}
}
The scaffolding plugin will be extended to allow the generation of RESTful controllers out of the box
A pluggable object rendering API will be provided with implementations that uses Grails' existing JSON / XML marshalling, but also allow implementations to added that use Jackson or libraries like GSon
If the respond
method is passed a domain or command object that has errors then an appropriate error response will automatically be returned. Error renderers will be customizable with a build in option to support vnd.error
The existing MimeType API will be extended to make it easy to define custom content types as encourages by HATEOAS principals.
An @Resource annotation will be added that will transform domain instances adding an API to create Atom / HAL links.
@Resource(contentType="application/vnd.books.org.book+json")
class Book {
String title
}
def b = new Book()
b.link(resource:"author", rel:"Book Author", contentType: Author.CONTENT_TYPE)
The respond
method covered earlier will detect the presence of the annotation and use the HATEOS resource representation if it exists.
References: