Jester 1.2: Flexible REST

Posted by Eric Mill

Apr 30

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:


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.

1
2
>>> 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.

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

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

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

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

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

>>> eric.unique_key
"frederick"

Controller:

1
2
3
4
5
6
7
8
9
10
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. ”<email></email>”) 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:

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

Suggestions and feedback much appreciated as usual!


Comments on this post

samleb

Apr 30

samleb said,

Absolutely great :) Thank you so much guys

Ben Schwarz

Apr 30

Ben Schwarz said,

Sounds good (without testing it) I’d started writing my own implementation of jester that used JSON instead of XML (for speed) like the day after jester came out… got busy and dumped it.. you basically rolled in everything that I started to want out of my own code. Well done.

Eric Mill

Apr 30

Eric Mill said,

Thanks guys. Ben, the next version of Jester will support JSON, you’re not the first person to indicate a desire for it.

Nicholas Barthelemy

Apr 30

Nicholas Barthelemy said,

This is a really exciting release. Jester has been a project with huge potential right from the start. Now I believe it is in a state that will allow people to truly recognize it’s usefulness.

Alex MacCaw

May 01

Alex MacCaw said,

Great stuff! Who knows, this might even be included in the Rails core someday….

Ben Schwarz

May 01

Ben Schwarz said,

I knew this Eric, however with Jester adding alot of weight through XML parsing I thought for my purpose that keeping everything to strictly native js would mean a simple solution.

It was more one of those afternoon personal challenges of ‘lets get this working’. If I ever pick it up again I’ll forward it on to you.

Alex MacCaw

May 21

Alex MacCaw said,

Just thought I’d let you guys know of a bug I found with your ‘reduced’ version of objtree. Xml looking likes this: <lock_version>0</lock_version> is parsed to nil. Also, is it really necessary to grab the information from the server, update and then save it. Surely it’s quicker just to make an ‘asset’ object, update that and save it – that’s what Active Resource does – it doesn’t need to know your schema/model information. Anyways, thanks for the great work!

Ryan Schuft

May 23

Ryan Schuft said,

Nice work! Looks like another great release!


Sorry, comments are closed for this article.

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