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.
>>> 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. ”<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:
// 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!