Friday, March 1, 2013

EmberJS - Putting a REST service behind the TodoMVC app

Background
I'm trying to select a front-end framework for new apps - who isn't these days? - and in researching I came across Addy Osmani and company's fantastic bit of work:  http://addyosmani.github.com/todomvc/

After looking at this and some other stuff I decided to give Ember a try.

So why not start by hooking up a real REST service to this ToDo app?  But first I would need to understand how Ember communicates with the server.  A little research came up with the JSON Interface section in this post.

Implementation
I'm working with a Grails server, so providing the REST interface was pretty easy, in theory.

In practice, I decided to use a plugin for the REST interaction, and chose the json-rest-api for its simplicity.  However I soon found out that it would not work out of the box - it needed upgrading to Grails2.1, and it did not speak the dialect of REST that Ember prefers.

So I set about modifying the plugin to support Ember-style, and maintain its original style as well. The resulting fork of the json-rest-api plugin is here.

Here are steps I took:

create grails app
  • grails create-app todo
  • add Todo domain class with fields from TodoMVC todo.js

install json-rest-api
  • added grails.plugin.location to the BuildConfig 
    • I already had this project downloaded locally
    • using grails.plugin.location means changes to the plugin are automatically picked up
        // Adding Plugin-in:  grails-json-rest
        grails.plugin.location.jsonrest = '/opt/projects/grails-json-rest-api'
    
  • changes to Todo domain class:
    • add static 'expose' to Todo domain class
    • add toJSON() and fromJSON() methods - my enhancement to the json-rest-api plugin to support i18n and custom rendering
class Todo {
    
    String title
    boolean isCompleted

    static constraints = {
        title(blank:false, nullable:false,maxSize:64)
        isCompleted(default:false)
    }
    
    
    String toString() {
        StringBuilder sb = new StringBuilder()
        sb.append("\n    id:    ").append(id)
        sb.append("\n    Title:    ").append(title)
        sb.append("\n    Completed:    ").append(isCompleted)
        sb.toString()
    }
    
    
    // --- json-rest-api artifacts --- 
    
    static expose = 'todo' // Expose as REST API using json-rest-api plugin
                           //   this will be the entity name on the URL
    static api = [
        // If allowing json-rest-api to use 'as JSON' to render, you may exclude
        //    unwanted fields here (done with its registered ObjectMarshaller)
        excludedFields: [ "attached", "errors", "properties" ],
        // You may override how the list() operation performs its search here
        list : { params -> Todo.list(params) },
        count: { params -> Todo.count() }
    ]

    
    /*
    // This is the standard way to override JSON marshalling for a class
    //    It uses a ClosureOjectMarshaller[sic] to select fields for marshalling 
    //    It is less efficient for the plugin which is based on JSONObject, but this will be
    //    used if you do not define a 'toJSON' method.
    // NOTE: if using this approach, the json-rest-api marshaller will NOT be used, hence the
    //      api.excludedFields if defined will be ignored
    // Example taken from http://grails.org/Converters+Reference
    static {
        grails.converters.JSON.registerObjectMarshaller(Todo) {
           // you can filter here the key-value pairs to output:
           return it.properties.findAll {k,v -> k != 'passwd'}
          }
    }
    */
    
    /**
     * Rending this object into a JSONObject; allows more flexibility and efficiency in how
     * the object is eventually included in larger JSON structures before ultimate rendering;
     * MessageSource offered for i18n conversion before exporting for user audience.
     * @param messageSource
     * @return
     */
    JSONObject toJSON(def messageSource) {
        JSONObject json = new JSONObject()
        json.put('id', id)
        json.put('title', title)
        json.put('isCompleted', isCompleted)
        return json
    }

    /**
     * Custom bind from JSON; this has efficiency since the grails request.JSON object offers
     *    a JSONObject directly
     * @param json
     */
    void fromJSON (JSONObject json) {
        [
            "title"
        ].each(JSONUtil.optStr.curry(json, this))
       [
            "isCompleted"
        ].each(JSONUtil.optBoolean.curry(json, this))
    }
}

install functional testing plugin
  • grails install-plugin functional-test
  • required to test the json-rest-api plugin (my change)

Added logging into Config.groovy

inside the environment {} block; also added similar to development {}

    
test {
        grails.logging.jul.usebridge = false
        log4j = {
            appenders {
                rollingFile name:"todo", maxFileSize:"10000KB", maxBackupIndex:10, file:"logs/todo.log",layout:pattern(conversionPattern: '%d{yyyy-MM-dd HH:mm:ss,SSS z} [%t] %-5p[%c]: %m%n')
                console name:'stdout', layout: pattern(conversionPattern: '%d{dd-MM-yyyy HH:mm:ss,SSS} %5p %c{1} - %m%n')
                //console name:'stacktrace'
            }
            
            debug 'grails.app','com.gargoylesoftware.htmlunit.WebClient','org.grails.plugins.rest',additivity = true
            warn 'grails.app.services.grails.buildtestdata','BuildTestDataGrailsPlugin','grails.buildtestdata',
                  'org.codehaus.groovy','org.grails.plugin','grails.spring','net.sf.ehcache','grails.plugin',
                  'org.apache','com.gargoylesoftware.htmlunit','org.codehaus.groovy.grails.orm.hibernate','org.hibernate'
            
            root {
                debug 'stdout', 'todo'
                additivity = true
            }
        }
    }

create Todo functional test
  • using a Generic Mixin test class that I added into the json-rest-api project, resulting functional test class looks like:

@Mixin(GenericRestFunctionalTests)
class TodoFunctionalTests extends BrowserTestCase {

    def log = LogFactory.getLog(getClass())
    def messageSource

    void setUp() {
        super.setUp()
    }
    
    void tearDown() {
        super.tearDown()
    }

    
    void testList() {
        genericTestList(new Todo(title:"title.one"))
    }
    
    void testCreate() {
        genericTestCreate(new Todo(title:"title.one"))
    }
    
    void testShow() {
        genericTestShow(new Todo(title:"title.one"))
    }
    
    void testUpdate() {
        genericTestUpdate(new Todo(title:"title.one"), [title:"title.two"])
    }
    
    void testDelete() {
        genericTestDelete(new Todo(title:"title.one"))
    }
}
 
Wiring up the Ember interface
Time to modify the TodoMVC project to hook it up to my Grails app. 
  • pulled the TodoMVC source into my Grails project
  • modified store.js to configure the REST adapter - default is DS.RESTAdapter, but some changes were required:
  • modified the namespace to match my context and path that json-rest-api listens to (/api)
  • had to extend the built-in RESTSerializer to stop its crazy conversion of my camel-case field names into underscore versions
    // Override the default behaviour of the RESTSerializer to not convert
    //   my camelized field names into underscored versions
    Todos.TodoRESTAdapter = DS.RESTSerializer.extend({
       keyForAttributeName: function(type, name) {
         return name;
         //return Ember.String.decamelize(name);  // this is the default behaviour
       },
    
       keyForBelongsTo: function(type, name) {
         var key = this.keyForAttributeName(type, name);
    
         if (this.embeddedType(type, name)) {
           return key;
         }
    
         return key + "Id";
       },
    
       keyForHasMany: function(type, name) {
         var key = this.keyForAttributeName(type, name);
    
         if (this.embeddedType(type, name)) {
           return key;
         }
    
         return this.singularize(key) + "Ids";
       }
    });
    
    
    Todos.Store = DS.Store.extend({
     revision: 11,
        adapter: DS.RESTAdapter.create({ 
         bulkCommit: false, 
         namespace: "todo/api",
         serializer: Todos.TodoRESTAdapter
        })
    });
    
  • found out that Ember sends the entity name to the server in the plural form sometimes, and the json-rest-api plugin does not like this; modified the plugin to account for this. See this other post for the breakdown of Ember's REST dialect.





  • Found out that running in Tomcat7 did not allow changes to my JS files (grrr....)

    • Ran instead via grails run-app, and added a logger to the 'development' env in Config.groovy in support of this
    • (yes I tried fixing Tomcat by disabling caching/antiLocking in the servlet context)

    Result

    And voila - after modifying the REST plugin, the Grails app was pretty easy to accomodate to the Ember pulls. The resulting app is located here.

    Installing

    Download the app
    git clone https://github.com/kentbutler/todomvc-grails-emberjs.git

    Download the fork of the grails-json-rest-api (at least for now)
    git clone https://github.com/kentbutler/grails-json-rest-api.git

    Place them alongside each other and test the app by opening a console inside the directory and run:
    grails test-app -functional

    If tests pass then run the app via
    grails run-app

    If they do not pass, ensure the path to the json-rest-api inside of grails-app/conf/BuildConfig.groovy accurately locates the grails-json-rest-api plugin.



    1 comment: