Jester 1.5: Universal REST

Eric Mill

It’s been quite some time since the last Jester release, but there’s actually been a great deal of work done on it since then. Jester’s object hierarchy code has been completely rewritten, some major new features added, and some syntax changes that break backwards compatibility. This release emphasizes working with customized server-side REST APIs, not just the ones generated by default in Rails through scaffold_resource. Jester is also moving beyond being just a JavaScript ActiveResource clone, with its own ideas that takes advantage of what JavaScript can do.

Bigger than that, this release coincides with the launch of a real Jester website, located at http://jesterjs.org. It’s a dirt simple site, with a basic howto, a download link, and a link to the new Jester discussion group. Maybe someday it’ll have its own blog, or its own Trac, or something, but the small approach seems to fit Jester snugly for now.

Jester is available from SVN in trunk form, or a 1.5 release form. You can also download a zipped copy of 1.5. Jester is released under the MIT License.

New features this release:

  • Path prefixes can interpolate keys, such as those for scoped resources (e.g. /users/:user_id)
  • Customization of all the URLs a model uses, such as its show URL, its destroy URL, etc.
  • Support for JSON callbacks (JSONP), allowing fetching of remote data if the API supports it
  • Renamed Base to Resource, and provided a scoped reference to it, Jester.Resource
  • Rewrite of object hierarchy, so objects returned from User.find are of class User
  • Models can fetch their skeleton from new.xml or new.json if passed checkNew: true on the call to Resource.model, instead of on each call to build
  • JSON responses will now have any date ending in _at or _on transformed into a Date value.
  • XML parser is only instantiated if a model is instantiated with an XML format

If you’re scoping your resources, by parent or date or whatever, you can use Ruby symbol notation to interpolate different keys into the URL. You can pass in values for these keys as part of the params hash that is the second argument to find. Any non-interpolated values will be appended to the query string.

>>> Resource.model("Article", {prefix: "/:section"})
Article()
>>> Article.find("first", {section: "humor"})
GET http://localhost:3000/humor/articles.xml

>>> Resource.model("Comment", {prefix: "/posts/:post_id"})
Comment()
>>> Comment.find("all", {post_id: 1, approved: true})
GET http://localhost:3000/posts/1/comments.xml

You can also go further, and specify a custom URL for each RESTful action for a given model, by providing a hash of URLs when you first define the model. You need only define the URLs which differ from the defaults. You can specify URLs for create, list, destroy, update, show, and new.

Resource.model("Article", {
  urls: {
    list: "/:section/articles.xml",
    show: "/all/articles/:id.xml",
    create: "/:section/new_article.xml"
  }
}

// will interpolate the section into the URL, and POST the rest as attributes
>>> article = Article.create(
  {section: "humor",
    title: "Fishing Through the Ages",
    author: "fishing@breathoffire.com"
  }
)
POST http://localhost:3000/humor/articles.xml

The coolest new feature is that Jester can now work with remote APIs that support JSON callbacks. This works by adding a script element to the DOM that loads in remote JavaScript, with a callback method name appended to the query string. The loaded JavaScript will call this method with the JSON representation of the remote data. Some people have gone ahead and started calling this method JSONP.

The poster child for this approach is Twitter. Here’s a working Twitter client:

Resource.model("Twitter", {
  format: "json",
  prefix: "http://twitter.com",
  urls: {
    list: "/statuses/user_timeline/:username.json",
    show: "/statuses/show/:id.json"
  }
}

There are some caveats here. Because loading remote data does not use XmlHttpRequest, no such object is returned from calls to find(). In addition, all calls to find() must be asynchronous, meaning you must provide a callback method. Lastly, since only GET requests are possible, the only operation you can perform on remote models is find().

// Loads
// http://twitter.com/statuses/user_timeline/jesterjs.json?callback=jesterCallback
// into the DOM
>>> Twitter.find("all", {username: "jesterjs"}, listTwitters)

// Loads
// http://twitter.com/statuses/show/12345678.json?callback=jesterCallback into
// the DOM
// Twitter.find(12345678, showTwitter)

Implementing this on the server is dirt easy in Rails, simply append a :callback parameter to your render :json call. This only works with render :json, not render :text. Here’s an example index action for a UsersController:

def index
  @users = User.find :all
  respond_to do |wants|
    wants.xml {render :xml => @users.to_xml}
    wants.json {render :json => @users.to_json, :callback => params[:callback]}
  end
end

Jester can take advantage of an API that provides a new template for an object, by setting checkNew to true as a parameter to Resource.model. Before, this was passed in on every call to build, which was completely unnecessary. The request to fetch this template will occur, immediately and asynchronously, after the call to Resource.model, and the template will be cached in the model and given to each object created with it.

>>> Resource.model("User", {checkNew: true})
User()
>>> eric = User.build()
GET http://localhost:3000/users/new.xml

// This only works because the User class knows "email" is an attribute
>>> eric.email = "emill@thoughtbot.com"
"emill@thoughtbot.com"
>>> eric.save()
POST http://localhost:3000/users.xml
true

I’m still open to ideas for a better parameter name than checkNew.

I have gotten bug reports, feature requests, and even patch submissions, and talked with people face-to-face who have heard of Jester and used it. Still, Jester began as a proof of concept, and has largely remained so. I think Jester has moved past the proof-of-concept stage; this release and the new website will be as good a test as any.

Visit our Open Source page to learn more about our team’s contributions.