Grails 2.3: REST Improvements - grails/grails-core GitHub Wiki

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

Proposal: REST Client Improvements

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).

Low-Level client

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.

High-level Client - GORM for REST

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.

Proposal: REST Server Improvements

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 Enhancements

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

Nested Resources

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

Link Generation Enhancements

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.

Response Rendering Enhancements

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)
    }
}

Automatic Binding to command and domain classes from JSON / XML / Params

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
        }
    }
}

Extended REST Scaffolding plugin (external to core)

The scaffolding plugin will be extended to allow the generation of RESTful controllers out of the box

Pluggable Renderers

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

Error Rendering & Vnd.Error

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

HATEOAS / Atom / HAL Support

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:

⚠️ **GitHub.com Fallback** ⚠️