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:


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.

inflection-js Home

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];
}
There’s still several things I have in mind for Jester. These are my next targets, in order of importance:

As always, your suggestions and general feedback are very much appreciated.


Comments on this post

Dr Nic

Apr 16

Dr Nic said,

I definitely look forward to playing with this.

Tammer Saleh

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.

Ryan Schuft

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.

Alex MacCaw

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.

Nicholas Barthelemy

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.

Eric Mill

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:

admins = User.find(:all, :admin => 1)

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.

Eric Mill

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.

Nicholas Barthelemy

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.

Ryan Schuft

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.

Ryan Schuft

Apr 18

Ryan Schuft said,

For some reason I thought wiki markup code would work in that last comment. What I intended was this:

User.find( ‘all’, { conditions:{ admin: 1 }, ajax:{ onSuccess: logAdmins } } )
Nicholas Barthelemy

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.

Nicholas Barthelemy

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.
Eric Mill

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
  end

If you do this, then the ID should be filled in before save returns, and reload will work as planned.

Nicholas Barthelemy

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.

theIntuitionist

Apr 18

theIntuitionist said,

hot. beautiful, clean, elegant, and hot.

Dan Kubb

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.

Eric Mill

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! :)

Jon Yurek

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.

Dan Kubb

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.

Eric Mill

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.

Dan Kubb

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