Jester 1.1: Asynchronous REST

Eric Mill

Since first releasing Jester, we’ve gotten a fantastic amount of interest and support, and even some donated code. In response, I’ve been whipping Jester into more powerful shape this week. The syntax now even more closely resembles ActiveResource’s and ActiveRecord’s, and adds the ability to use Jester asynchronously.

Jester is available from SVN in trunk form, or a 1.1 release form. You can also download a zipped copy of 1.1.

Jester is released under the MIT License.

More completely, today’s release of Jester 1.1 includes:

  • Asynchronous support
  • find(‘all’), find(‘first’) – Works like ActiveRecord.
  • attributes() – Works like ActiveRecord.
  • reload() – Works like ActiveRecord.
  • Pluralization, a proper Inflector library. (Thanks to Ryan Schuft)
  • Cleaned up and expanded JsUnit tests.
  • Significant code cleanup and prettifying.

To trigger asynchronous mode, a callback can be provided as an optional ending argument to find, save, create, reload, and destroy. This callback will be passed as an onComplete option to Prototype, and will be called regardless of success or failure. The callback function will be passed the same return result you’d expect if you had called the function synchronously. The asynchronous versions all return the underlying AJAX transport object, which in Firefox is an XMLHttpRequest.

>>> User.find(1, function(user) {eric = user})
GET http://localhost:3000/users/1.xml
XMLHttpRequest

>>> eric
Object _name=User _singular=user _plural=users
>>> eric.email = "beverly@cleary.com"
"beverly@cleary.com"

>>> eric.save(function(saved) {result = saved;})
POST http://localhost:3000/users/1.xml
XMLHttpRequest
>>> result
true

>>> User.find(eric.id).email
GET http://localhost:3000/users/1.xml
"beverly@cleary.com"

You can use find(‘all’) to get an array of all objects, and find(‘first’) to perform this same find, but automatically return only the first one. These two calls perform the same request—find(‘first’) simply discards the rest of the array. To me, this is better than requiring the controller to support a “limit” parameter.

Use reload to refresh an object’s data from the remote service. You can also get at a hash of an object’s attributes directly if you care to.

>>> User.find('all')
GET http://localhost:3000/users.xml
[Object _name=User _singular=user _plural=users,
  Object _name=User _singular=user _plural=users]

>>> eric = User.find('first')
GET http://localhost:3000/users.xml
Object _name=User _singular=user _plural=users

>>> eric.email
"emill@thoughtbot.com"

>>> eric.attributes()
Object active=true email=emill@thoughtbot.com id=1
>>> eric.attributes().email
"emill@thoughtbot.com"
>>> eric.email = "beverly@cleary.com"
"beverly@cleary.com"

>>> eric.reload()
GET http://localhost:3000/users/1.xml
Object _name=User _singular=user _plural=users

>>> eric.email
"emill@thoughtbot.com"

Ryan Schuft contributed a port of Rails’ Inflector, for JavaScript. It’s superior to the existing Inflector code libraries I’ve seen on the Internet, and is a welcome edition. He released inflector-js over the weekend, you can pick it up there. Jester is currently only using pluralize, but there are many great features to it. Thanks so much to Ryan for contributing to Jester, and for a great string library to the JavaScript community.

inflection-js Home

>>> Base.model("Person")
>>> Person.find('all')
GET http://localhost:3000/people.xml

The JsUnit tests were put through a lot of work, and are now more complete and more readable. Interestingly, without intending to, I ended up creating something very close to the HttpMock test framework that the ActiveResource team did for ActiveResource. My mock Internet looks like this:

Internet = {}
Internet[User._singular_url(1)] = {'user': ericDetails};
Internet[Post._singular_url(1)] = {'post': postDetails};
Internet[User._plural_url()] = allUsersDetails;

// used to make sure callbacks get called
changed = false;
change = function() {changed = true;}

Base.tree.parseHTTP = function (url, options, callback) {
  call = function(doc) {change(); return callback(doc);}
  if (callback)
    return call(Internet[url]);
  else
    return Internet[url];
}

There’s still several things I have in mind for Jester. These are my next targets, in order of importance:

  • Scoped URL prefixes (e.g. /users/1/posts/1.xml).
  • In addition to allowing a simple callback, allow a full options hash to also trigger asynchronous mode. This hash would be passed directly into Prototype’s Ajax.Request constructor. This would allow greater control over callbacks, including onSuccess, onFailure, etc.
  • Pack everything into only one file, jester.js, including only what I need from Prototype and ObjTree. This will mean a smaller load time, and only one script include line in your view HTML.
  • Automatic date parsing. Dates are still returned as a string, as JavaScript’s native Date.parse does not understand Rails’ timestamp encoding.

As always, your suggestions and general feedback are very much appreciated.