GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Jester 1.2: Flexible REST

This release is about making Jester more flexible, and better supporting custom REST APIs. The flurry of activity in ActiveResource is a good reminder that REST isn’t just several default controller actions—it’s a guiding philosophy to defining your own API. REST is just about using simple URLs and HTTP status codes to carry all the metadata, so that the bodies of your requests and responses don’t have to.

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

New features this release:

  • find() will take a hash of query parameters to append to the URL.
  • Base.model() takes all parameters after the name as a hash, instead of a list.
  • Asynchronous calls can take a hash of callbacks and options, instead of just a callback.
  • Support for reading an object’s schema. (new.xml)
  • Dates are parsed into Date objects. (Thanks to Nicholas Barthelemy)
  • Objects are reloaded from XML if XML is returned after a create or update request.
  • Included ObjTree.js inline, and replaced prototype.js with an optional lighter form (prototype.jester.js).


You can pass arbitrary query parameters along with a find request, in a hash. It’s the second parameter, bumping the asynchronous callback/options parameter to third. This breaks backwards compatibility.

>>> User.find("all", {admin: true, toys: 5})
GET http://localhost:3000/users.xml?admin=true&toys=5

Arguments when defining a model are now taken as a hash instead of a plain ordered list. This breaks backwards compatibility.

>>> Base.model("User", {plural: "people", prefix: "http://www.thoughtbot.com"})
>>> User._plural_url()
"http://www.thoughtbot.com/people.xml"

You can now supply a hash of options to be fed directly to Prototype’s Ajax.Request method, instead of just a callback. You can use this to specify different callbacks for success or failure conditions, or override the HTTP method used in the request, or anything specified here. If you supply only a callback, it will be treated as your “onComplete” callback. You can use these options with a synchronous request if you set “asynchronous” to false.

>>> User.find(1, {}, {onSuccess: success, on404: notFound, method: "post"})
POST http://localhost:3000/users/1.xml

>>> User.find(1, {}, successCallback)
GET http://localhost:3000/users/1.xml

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

There is a longstanding problem in Jester, which is that when you create or build a new object, you have to specify all of its properties in the attributes hash. If you simply call eric = User.build(), and then later, eric.name = "Eric", the User model has no way of knowing this is a model attribute, and it won’t be included in any save() requests. ActiveResource gets around this by using Ruby’s method_missing, which isn’t an option for clients written in many languages.

After talking it over with my coworkers, we realized there was value in giving a REST client access to a model’s schema, much as ActiveRecord has database access to ascertain a model’s schema. So to I proposed that Rails introduce into their standard REST controllers the “new.xml” route, and a patch with it, which was accepted this week.

Jester supports this, but it is disabled by default, as it will incur an HTTP request when you may not expect it, and your code may work fine without it. It also currently only works synchronously. You can trigger it by passing an option to build in a second hash parameter.

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

>>> eric._properties
["active", "email", "name"]

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

>>> eric._properties
["active", "email", "name", "lasers"]

Dates are now parsed into actual JavaScript Date objects when a model is loaded from XML, thanks to code contributed by Nicholas Barthelemy. (SVN)

>>> post = Post.find(1)
GET http://localhost:3000/posts/1.xml
>>> post.created_at
Sat Mar 31 2007 03:01:56 GMT-0500 (Eastern Standard Time)

If a create or update request results in an XML response body, the model will reload itself using this XML. So, if your app changes an object on create or on update, this will be reflected in the client, as long as your controller renders the object in XML after saving it. Props to Nicholas Barthelemy for pointing out this was important before DHH suddenly committed changes in ActiveResource to do the exact same thing.

Client:

>>> eric = User.create({email: "emill@thoughtbot.com", name: "Eric Mill"})
POST http://localhost:3000/users.xml

>>> eric.unique_key
"frederick"

Controller:


def create
  @user = User.new(params[:user])
  @user.unique_key = "frederick"
  
  respond_to do |format|
    if @user.save
      headers["Location"] = user_url(@user)
      format.xml  { render :xml => @user.to_xml, :status => 201}
  end
end

You no longer need to include ObjTree.js in your own HTML. I took the parts of ObjTree I used, packed them using Dean Edwards’ packer script, and appended them to jester.js. In the same vein, I removed prototype.js from the repository, and replaced it with a smaller form of prototype, prototype.jester.js. Use this if you aren’t using Prototype for anything besides Jester, and want quicker loading times.

There were also some little fixes. If a resource is found by its ID, the ID will definitely be set in the object’s properties, even if the returned XML didn’t include it. Also, the ID is set more correctly when parsed out of a Location header, though I doubt this issue affected anyone, as it still worked fine in practice in most cases. There was also a bug in ObjTree where empty attributes (i.e. ””) weren’t even counted.


There’s still some significant work left for Jester, and I’m sure even more great ideas will come out of you guys, and out of the ActiveResource team. My targets for the next release, in order of importance:

  • Allowing “prefix options”, so that you can specify comments at /users/:user_id/comments.xml, instead of /comments.xml. Options would be passed inside the same hash as the query parameters, and separated by Jester. An example might look like:
// find all approved comments by user #1
>>> Comment.find("all", {user_id: 1, approved: true})
GET http://localhost:3000/users/1/comments.xml?approved=true

  • Calls to new.xml will only happen once per model, when the model is first declared, not on every call to build.
  • Support for “custom methods”, as specified in ActiveResource here
  • Support for manually specified URLs, as specified in ActiveResource here
  • Provide an option for a model so that Jester will submit POST and PUT request parameters as an XML body, instead of as form parameters.

Suggestions and feedback much appreciated as usual!