Jester 1.5: Universal REST

Posted by Eric Mill

Oct 25

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:


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.

1
2
3
4
5
6
7
8
9
>>> 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”.

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
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()”.

1
2
3
4
5
// 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:

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
10
11
>>> 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.


Comments on this post

Dr Nic

Oct 26

Dr Nic said,

Oooh, nice new site.


Sorry, comments are closed for this article.

© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots