You are building a boring blog application. A blog has many comments. Your client gives you this user story:
So that I can report inappropriate comments, as a user, I can mark a comment as spam.
So you add a spam boolean attribute to the Comment model, and use the same PUT to comments to handle the update:
class CommentsController < ApplicationController
def update
@comment = Comment.find(params[:id])
if @comment.update_attributes(params[:comments])
flash[:notice] = "Comment updated successfully"
# redirect_to ...
else
flash[:error] = "Something broke"
# redirect_to ...
end
end
end

By sending the right params down from the view, this can still work. But a day passes by and people start abusing the darn spam button. This is the internet after all. So your client adds a couple of stories:
So that I’m aware I screwed up, as a user, I receive an email notification when my comments get spammed.
So that I can moderate spammed comments, as an admin, I receive an email notification when comments are spammed.
There’s now more going on when we spam a comment. A first shot at implementing these stories could make use of ActiveRecord’s dirty attributes to figure out if the spam boolean changed from false to true, in which case we deliver the two notifications:
class Comment < ActiveRecord::Base
before_save :send_spam_notifications, :if => marking_as_spam?
def send_spam_notifications
Mailer.comment_owner_spam_notification(self).deliver
Mailer.admin_spam_notification(self).deliver
end
def marking_as_spam?
self.spam_changed? && self.spam?
end
end
Nice and clean, right? Wrong.I think there’s a couple of problems with this. First of all, we are cluttering our comment class with conditional callbacks. Sometimes you have no choice, but I’d rather avoid them when possible. Secondly, we’re using the same HTTP endpoint and controller action for two very different things. One is that of editing my comment, and the other is for marking a comment as spam — which is arguably an SRP violation (PDF link).
We have a couple of options for cleaning up. We could create a #update_spam_status action on the comments controller, but this deviates from our otherwise RESTful API. Instead, let’s break it out to it’s own resource called Spams, and instead of a PUT to comment, let’s POST to spam:
class SpamsController < ApplicationController
def create
@comment = Comment.find(params[:id])
if @comment.mark_as_spam
flash[:notice] = "Comment marked as spam"
# redirect_to ...
else
# ...
end
end
end
class Comment < ActiveRecord::Base
def mark_as_spam
self.spam = true
send_spam_notifications if valid?
save
end
end
Our view can make use of a button_to that POSTs to the spams resource, instead of relying on updating via the comments resource. Note how a RESTful resource does not need to match 1-to-1 with a model, and this creates a much cleaner interface overall — with a few advantages:
#mark_as_spam method that is clear and to the point, untangling some of the callback soup that can start to build up on models.#mark_as_spam’s implementation to anything else, like an Akismet call, or a more complex Spam class.@comment = current_user.comments.find(params[:id])), allowing any user to mark comments as spam, and only commenters to update their own comments.The question then arises, when should we split out to a new resource? As always, it depends. For the most part it’s whenever it will help clean up your models and controllers. I find that if the update triggers any logic at all, it’s worth breaking out to its own resource and corresponding model method that encapsulates that logic.
In an application we’re currently building, users go through a wizard to order teams. We implemented this as RESTful controllers – teams, purchases, and orders.
The relevant wizard steps are:
When each step is submitted, the related create action is called and the user is redirected to the next new action.
On step 3, the user hits the back button. If they re-submit step 2, they will get a validation error because they cannot create two purchases for a team.
There’s a temptation here to stray from RESTful design. Don’t!
When the user hits the back button from step 3, we want to send them to purchases/edit for their newly-created object instead of purchases/new.
To ensure that the page isn’t cached by the browser and will always be re-fetched, we add a before_filter to the purchases controller which calls a private method on ApplicationController:
before_filter :no_cache, :only => [:new]
private
def no_cache
response.headers["Last-Modified"] = Time.now.httpdate
response.headers["Expires"] = 0
# HTTP 1.0
response.headers["Pragma"] = "no-cache"
# HTTP 1.1 'pre-check=0, post-check=0' (IE specific)
response.headers["Cache-Control"] = 'no-store, no-cache, must-revalidate, max-age=0, pre-check=0, post-check=0'
end
Next, if the team already has a purchase, we redirect to the edit action using another before filter on the purchases controller:
before_filter :redirect_to_edit, :only => [:new], :if => :team_has_registration_purchase?
#team_has_registration_purchase? is a method on the purchases controller. If the :if syntax is unfamiliar to you, its provided to us by our plugin, when.
To the best of our knowledge, setting the HTTP headers should have worked in all browsers, but it did not work in Safari or in IE 6 and 7. A little more research proved that any page with an iframe on it will never be cached, and will always be refetched.
So, we add this to views/purchases/new:
<iframe style="height:0px;width:0px;visibility:hidden" src="about:blank">
this frame prevents back forward cache
</iframe>
This works. We now have cross-browser no-caching in a RESTful wizard.
The iframe hack feels a little dirty, though. Does anyone know a better way? Or, do we live with it, similar to using a hidden frame for “Ajax” file uploads with responds_to_parent ?
This concept might be useful outside of these more complex, wizard type controller actions, and might come in handy if you had just a normal restful controller where you wanted the user to get the edit action instead of the new action if they use the back button. Have you done something like this before, or can you think of a different way to accomplish that?
photo courtesy of Michael Porter via Flickr
Jester is our implementation of REST, in JavaScript. It provides (nearly) identical syntax to ActiveResource for using REST to find, update, and create data, but from the client side.
Update, 6/16/07: We have released version 1.3 of Jester. You may want to view its release description.
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.
All examples below are taken from inside the JavaScript console of Firebug, the best JavaScript development tool you could possibly have.
First, declare a model in Jester by calling model on Base:
>>> Base.model("User")
>>> User
Object _name=User _singular=user _plural=users
This creates a global variable called User. It assumes that the URL prefix it uses to base its HTTP requests from is the current domain and port, and assumes “user” and “users” as single and plural forms to make these URLs. There’s no “people/person” intelligence here, so make sure to override these defaults if you need to, like so:
>>> Base.model("Child", "http://www.thoughtbot.com", "child", "children")
>>> Child
Object _name=Child _singular=child _plural=children
If you want to capture the model created in a local variable, or simply prefer more traditional JavaScript syntax, you can do:
>>> var Child = new Base("Child", "http://www.thoughtbot.com", "child", "children")
>>> Child
Object _name=Child _singular=child _plural=children
Find will retrieve a particular instance of your model. Attributes are auto-converted to integer or boolean types if that’s what they are on the server side. The “GET” line is not a return value, just Firebug’s report of activity, but relevant to understanding what’s happening.
>>> eric = User.find(1)
GET http://localhost:3000/users/1.xml
Object _name=User _singular=user _plural=users
>>> eric.attributes
["active", "email", "id", "name"]
>>> eric.id
1
>>> eric.name
"Eric Mill"
>>> eric.active
true
Create takes a hash of attribute values. After calling create, the model will fetch its new ID from the return headers.
>>> floyd = User.create({name: "Floyd Wright", email: "tfwright@thoughtbot.com"})
POST http://localhost:3000/users.xml
Object _name=User _singular=user _plural=users
>>> floyd.id
9
>>> User.find(9).name
GET http://localhost:3000/users/9.xml
"Floyd Wright"
Updating is as simple as changing one of the properties and calling save
.
>>> eric = User.find(1)
GET http://localhost:3000/users/1.xml
Object _name=User _singular=user _plural=users
>>> eric.email
"emill@thoughtbot.com"
>>> eric.email = "sandybeach@wintermute.com"
"sandybeach@wintermute.com"
>>> eric.save()
POST http://localhost:3000/users/1.xml
true
>>> User.find(eric.id).email
GET http://localhost:3000/users/1.xml
"sandybeach@wintermute.com"
Sadly, there’s one area where Jester’s syntax can’t match ActiveResource’s perfectly. The method “new” has been renamed to build, due to “new” being an illegal method name in JavaScript up to 1.6. Hopefully this can be updated as the browser landscape evolves. Build was chosen because it is similarly used in ActiveRecord to replace “new” on an association array, where “new” cannot be used.
>>> chad = User.build({email: "cpytel@thoughtbot.com", name: "Chad Pytel"})
Object _name=User _singular=user _plural=users
>>> chad.new_record()
true
>>> chad.save()
POST http://localhost:3000/users.xml
true
>>> chad.id
9
>>> chad.new_record()
false
Error validations are supported. If a model fails to save, save returns false, and the model’s errors property is set with an array of the error messages returned.
>>> jared = User.build({name: "", email: ""})
Object _name=User _singular=user _plural=users
>>> jared.save()
POST http://localhost:3000/users.xml
false
>>> jared.errors
["Name can't be blank", "Email can't be blank"]
>>> jared.valid()
false
>>> jared.name = "Jared Carroll"
"Jared Carroll"
>>> jared.email = "emill@thoughtbot.com"
"emill@thoughtbot.com"
>>> jared.save()
POST http://localhost:3000/users.xml
false
>>> jared.errors
["Email has already been taken"]
>>> jared.email = "jcarroll@thoughtbot.com"
"jcarroll@thoughtbot.com"
>>> jared.save()
POST http://localhost:3000/users.xml
true
Lastly, associations are also supported. If the association data is included in the XML, they’ll be loaded into the returned model as Jester models of their own, using the same assumptions on naming and URL prefix described above. They’re full models, so you can edit and save them as you would the parent. Has_many relationships come back as simple arrays, has_one relationships as a property. In this example, User has_many :posts, and Post belongs_to :user.
>>> eric = User.find(1)
GET http://localhost:3000/users/1.xml
Object _name=User _singular=user _plural=users
>>> eric.posts
[Object _name=Post _singular=post _plural=posts, Object _name=Post _singular=post _plural=posts]
>>> eric.posts.first().body
"Today I passed the bar exam. Tomorrow, I make Nancy my wife."
>>> eric.posts.first().body = "Today I *almost* passed the bar exam. The ring waits one more day."
"Today I *almost* passed the bar exam. The ring waits one more day."
>>> eric.posts.first().save()
POST http://localhost:3000/posts/1.xml
true
>>> post = Post.find(1)
GET http://localhost:3000/posts/1.xml
Object _name=Post _singular=post _plural=posts
>>> post.body
"Today I *almost* passed the bar exam. The ring waits one more day."
>>> post.user
Object _name=User _singular=user _plural=users
>>> post.user.name
"Eric Mill"
Jester depends on two libraries: Prototype, which comes with Rails and most people are familiar with, and ObjTree, a nice DOM parsing engine for JavaScript. Both of these are packaged along with Jester in its SVN repository, so you don’t have to hunt for them yourself. Just make sure you’re including all three in your test file.
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<script type="text/javascript" src="/javascripts/ObjTree.js"></script>
<script type="text/javascript" src="/javascripts/jester.js"></script>
JavaScript in the browser is limited to requests with in only the same domain as the script is running in, so without iframe hackery, Jester is probably only useful for writing client code in your own apps, to talk to itself. We’re investigating whether Jester can use this hackery to make cross-domain requests, but it’s not clear if this will be feasible.
There are also some basic unit tests included inside Jester’s repository, which run using JsUnit. To run them yourself, from Jester’s repository open the file test/jsunit/testRunner.html in your browser, and choose test/jester_test.html as the test file.
These examples are talking with a Rails application whose controllers were generated with ”./script generate scaffold_resource”—in other words, the ideal RESTful controllers. It’s very easy to make your controller RESTful. Here’s the source for the User controller I’m using. The lines that deal with returning HTML have been removed, and I have added “(:include => :posts)” as an argument to to_xml in two places, so associations are included (it’s that easy!).
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<script type="text/javascript" src="/javascripts/ObjTree.js"></script>
<script type="text/javascript" src="/javascripts/jester.js"></script>
An example of the XML produced here, of a User with one Post, at /users/2.xml:
<user>
<active type="boolean">true</active>
<email>cpytel@thoughtbot.com</email>
<id type="integer">2</id>
<name>Chad Pytel</name>
<posts>
<post>
<title>Life as a Jester</title>
<body>It's not as hard as Master said it would be. Today I made 200 dollars.</body>
<created-at type="datetime">2007-04-01T04:01:56-04:00</created-at>
<id type="integer">2</id>
<user-id type="integer">2</user-id>
</post>
</posts>
</user>
To see some real live examples, the Beast forum is currently implenting some of ActiveResource. Here’s technoweenie’s User account, in XML, and an XML list of selected Users. Pretty much any URL in Beast can have ”.xml” appended to it.
Taking ARes Out for a Test Drive—Great introduction to ActiveResource, by one of its authors.
ActiveResource and Testing—A post I made here discussing how I tested ActiveResource models.
ActiveResource’s Subversion Repository—Current ActiveResource trunk, at svn.rubyonrails.org.
Thanks go to Chad for the original idea, and Jared for writing Jester’s tests.
Jester is new, so we’d love to hear feedback on its strengths and weaknesses. We’re using it ourselves, so it’s under active development and getting plenty of love and attention. Please tell us what you think!