Jester 1.1: Asynchronous REST
Posted by Eric Mill
Apr 16
Since first releasing Jester, we’ve gotten a fantastic amount of interest and support, and even some donated code. In response, I’ve been whipping Jester into more powerful shape this week. The syntax now even more closely resembles ActiveResource’s and ActiveRecord’s, and adds the ability to use Jester asynchronously.
Jester is available from SVN in trunk form, or a 1.1 release form. You can also download a zipped copy of 1.1. Jester is released under the MIT License.
More completely, today’s release of Jester 1.1 includes:- Asynchronous support
- find(‘all’), find(‘first’) – Works like ActiveRecord.
- attributes() – Works like ActiveRecord.
- reload() – Works like ActiveRecord.
- Pluralization, a proper Inflector library. (Thanks to Ryan Schuft)
- Cleaned up and expanded JsUnit tests.
- Significant code cleanup and prettifying.
To trigger asynchronous mode, a callback can be provided as an optional ending argument to find, save, create, reload, and destroy. This callback will be passed as an onComplete option to Prototype, and will be called regardless of success or failure. The callback function will be passed the same return result you’d expect if you had called the function synchronously. The asynchronous versions all return the underlying AJAX transport object, which in Firefox is an XMLHttpRequest.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
>>> User.find(1, function(user) {eric = user}) GET http://localhost:3000/users/1.xml XMLHttpRequest >>> eric Object _name=User _singular=user _plural=users >>> eric.email = "beverly@cleary.com" "beverly@cleary.com" >>> eric.save(function(saved) {result = saved;}) POST http://localhost:3000/users/1.xml XMLHttpRequest >>> result true >>> User.find(eric.id).email GET http://localhost:3000/users/1.xml "beverly@cleary.com" |
You can use find(‘all’) to get an array of all objects, and find(‘first’) to perform this same find, but automatically return only the first one. These two calls perform the same request—find(‘first’) simply discards the rest of the array. To me, this is better than requiring the controller to support a “limit” parameter.
Use reload to refresh an object’s data from the remote service. You can also get at a hash of an object’s attributes directly if you care to.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
>>> User.find('all') GET http://localhost:3000/users.xml [Object _name=User _singular=user _plural=users, Object _name=User _singular=user _plural=users] >>> eric = User.find('first') GET http://localhost:3000/users.xml Object _name=User _singular=user _plural=users >>> eric.email "emill@thoughtbot.com" >>> eric.attributes() Object active=true email=emill@thoughtbot.com id=1 >>> eric.attributes().email "emill@thoughtbot.com" >>> eric.email = "beverly@cleary.com" "beverly@cleary.com" >>> eric.reload() GET http://localhost:3000/users/1.xml Object _name=User _singular=user _plural=users >>> eric.email "emill@thoughtbot.com" |
Ryan Schuft contributed a port of Rails’ Inflector, for JavaScript. It’s superior to the existing Inflector code libraries I’ve seen on the Internet, and is a welcome edition. He released inflector-js over the weekend, you can pick it up there. Jester is currently only using pluralize, but there are many great features to it. Thanks so much to Ryan for contributing to Jester, and for a great string library to the JavaScript community.
1 2 3 |
>>> Base.model("Person") >>> Person.find('all') GET http://localhost:3000/people.xml |
The JsUnit tests were put through a lot of work, and are now more complete and more readable. Interestingly, without intending to, I ended up creating something very close to the HttpMock test framework that the ActiveResource team did for ActiveResource. My mock Internet looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Internet = {}
Internet[User._singular_url(1)] = {'user': ericDetails};
Internet[Post._singular_url(1)] = {'post': postDetails};
Internet[User._plural_url()] = allUsersDetails;
// used to make sure callbacks get called
changed = false;
change = function() {changed = true;}
Base.tree.parseHTTP = function (url, options, callback) {
call = function(doc) {change(); return callback(doc);}
if (callback)
return call(Internet[url]);
else
return Internet[url];
} |
- Scoped URL prefixes (e.g. /users/1/posts/1.xml).
- In addition to allowing a simple callback, allow a full options hash to also trigger asynchronous mode. This hash would be passed directly into Prototype’s Ajax.Request constructor. This would allow greater control over callbacks, including onSuccess, onFailure, etc.
- Pack everything into only one file, jester.js, including only what I need from Prototype and ObjTree. This will mean a smaller load time, and only one script include line in your view HTML.
- Automatic date parsing. Dates are still returned as a string, as JavaScript’s native Date.parse does not understand Rails’ timestamp encoding.
As always, your suggestions and general feedback are very much appreciated.
Comments on this post
Apr 16
Dr Nic said,
I definitely look forward to playing with this.
Apr 16
Tammer Saleh said,
This looks fantastic.
I can see why you decided not to force the controller to implement a new :limit param, but that may cause problems when working with large datasets. Could jester pass the limit param to the controller, as well as discard the rest of the results? That way, applications with larger datasets could support :limit, while most others could just ignore it.
Apr 16
Ryan Schuft said,
If anyone wants the inflection library, you can get to the project site for it at http://code.google.com/p/inflection-js
It’s a JavaScript port of ActiveSupport’s inflector.rb and inflections.rb functionality.
Apr 17
Alex MacCaw said,
Brilliant! I’m already using it in one of my projects (aireo). What would be great though is support for custom actions. To allow this you’d need to let people append custom url endings and custom methods.
Apr 17
Nicholas Barthelemy said,
One thing that I noticed while trying to adapt an application to use Jester was the inability to explicitly set callback functions in async mode. Such as:this.__model.save({ onLoading: function(){ $(this.footer).hide(); }.bind(this), onComplete: function(req, result){ $(this.owner).update(); $(this.ts).update(this.options.timestamp); Effect ? Effect.Appear(this.footer,{ duration: 0.25 }) : $(this.footer).show(); }.bind(this) });This seems to be a trivial change in the jester code base that would make it much more flexible.
Apr 17
Eric Mill said,
Nicholas, you’re absolutely right. I listed this in my todos at the end of the post as something I’m going to do.
But, there is one major downside to this: I also want to be able to submit arbitrary parameters to calls like find(), using a hash. This is how ActiveResource does it:
And it expects the controller to use params[:admin] to search with. I’d like to have it work similarly in Jester…
admins = User.find("all", {admin: 1})But if I have the second option be a hash of parameters, it can’t also be a hash of AJAX options. It’s a conflict, and I’m open to suggestions of the best way to resolve it.
Apr 17
Eric Mill said,
After talking this over with my coworkers, I think the best solution is to take query parameters as the second argument to find, and have Ajax options (or a simple callback) taken as the third.
User.find('all', {admin: 1}, {onSuccess: logAdmins})It means submitting an empty hash as the second argument if you want to do a plain find with an Ajax callback, though. I don’t like this, but I’m willing to accept it.
Apr 17
Nicholas Barthelemy said,
I think it is less than elegant to pass the third param for a normal find, but it does make the API more clear for the find function. Would all of the other functions that accept callbacks take a hash or single function? Either way I think we are closing in on a unified solution.
Apr 18
Ryan Schuft said,
What about using this format?
{{{ User.find(‘all’, {conditions:{admin: 1}, ajax:{onSuccess: logAdmins}}) }}}
You could leave conditions or ajax out and pass as much or as little as you wanted.
Apr 18
Ryan Schuft said,
For some reason I thought wiki markup code would work in that last comment. What I intended was this:
Apr 18
Nicholas Barthelemy said,
I have been working with Jester today and one other issue I have run into is that it will not handle an XML return value on a save call. It simply returns true or false. This becomes behavior becomes insufficent when you would like to update a property on your client-side object based on a calculation or query that the server has done with the changed dataset. Currently, you would have to reload the object once the update is made. This would require two requests. I am currently looking at a way for save to detect an XML response and update the object.
Apr 18
Nicholas Barthelemy said,
One more thing to add that I just noticed. According to the following block of code (and my tests) when you save a new object and go to reload it Jester prevents you from doing so because it doesn’t have an id.reload: function(callback){ ... if (this.id) { if (callback) return this.find(this.id, reloadWork); else return reloadWork(this.find(this.id)); } else return this; }Which becomes another reason to have a parsing solution for the return value of the save function.Apr 18
Eric Mill said,
If you save a new object in Jester, or ActiveResource, the new ID will be autofilled in, if you return a header that contains the location of the new resource. Here’s the example controller code for #create I gave in my original Jester post:
# POST /users.xml def create @user = User.new(params[:user]) respond_to do |format| if @user.save format.xml { head :created, :location => user_url(@user) } else format.xml { render :xml => @user.errors.to_xml } end end endIf you do this, then the ID should be filled in before save returns, and reload will work as planned.
Apr 18
Nicholas Barthelemy said,
Great. Thank you. I apologize that I wasn’t more thorough in my examination of the documentation you have already provided, but I appreciate you pointing that out.
Apr 18
theIntuitionist said,
hot. beautiful, clean, elegant, and hot.
Apr 19
Dan Kubb said,
Is anyone working on Rails helpers that use Jester for things like AutoComplete? It seems as if the built-in Rails helpers don’t make use of the features available in RESTful Rails enabled apps.
Apr 19
Eric Mill said,
Dan, I’m not sure what you mean. And what you’re saying sounds potentially fascinating, so I’d like to know! :)
Apr 19
Jon Yurek said,
That could be a really good idea. Only problem is that it needs the conditions syntax to be ironed out, or there would be a lot of excess data transfer.
Apr 19
Dan Kubb said,
Eric, there was some talk over here about how Rail’s AutoComplete helper used an approach that doesn’t really fit with the RESTful Rails way of doing things.
In that article’s comments I mentioned I think a better approach would be to just use the standard #index action, and return XML using respond_to. On the client side it could be parsed and used to create an AutoComplete field, just like how AutoComplete works now.
Taking it a step further, I could see a benefit of writing a plugin that can act as a stand-in replacement for normal Rails javascript helpers (like AutoComplete) but using Jester and RESTful Controllers.
My comment above was mainly to see if anyone’s gone down this road or if there’s any interest in this area.
Apr 20
Eric Mill said,
I think you and nap are both right on, Dan. I suspect your reasoning, that it’s because the AutoComplete helpers were around well before Rails folk started thinking in terms of REST all the time, is also correct.
I should look into the JavaScript helpers in Rails. What other ones besides AutoComplete do you know of in rails that don’t act RESTfully? A “Restful JavaScript” plugin for Rails might not be out of order if it’s significant.
May 03
Dan Kubb said,
Eric, the main helpers that could be better done RESTfully are the ones for AutoCompleter and InPlaceEditor. In the docs it says they’ll be pulled out of the core into plugins before Rails 2.0 is released.
In AutoCompleter’s case it would be better to do a GET to the #index action, retrieving the xml formatted data to merge into the autocompletion field. For InPlaceEditor, it would be better to do a PUT to the #update action.
Actually, the worst part about these is that they’re paired with other helpers that dynamically add actions to the controllers to handle their respective requests. Nasty.
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives