Time to continue the REST and Rails theme.
Rails’ most egregious violation of REST is that it exposes object methods
http://myrailsapp.com/users/show http://myrailsapp.com/users/list http://myrailsapp.com/users/update
Remember, one of the fundamental ideas of REST is to manipulate resources (represented by URIs) via the use of a limited set of verbs. For our purposes, the verbs that matter are HTTP’s GET, POST, PUT and DELETE.
Rails does not do this, instead it exposes public object methods on the Internet. This is no different than what DCOM, CORBA, RMI, XML-RPC, SOAP and your own home-grown RPC (remote procedure call) system do. And yes, every good developer has written an RPC system or two. In fact, I think that’s the main problem. Every developer I’ve ever met who writes a distributed computing system always implements some sort of RPC mechanism. I know I did it the first time. Its seems perfectly logical – let’s hide away the nasty details of the network so the developer doesn’t have to worry about it. Of course it doesn’t work, but that doesn’t mean that 9 out of 10 developers will tell you it does.
The good news of course is that RAILS is built on top of the Web, which is one of the most successful distributed computing system created and is not an RPC system. In addition, RAIL’s RPC implementation is much simpler to use than the ones I mentioned above and it strongly encourages you to create nice looking URIs which is a significant benefit.
So at this point, unless you’re a RESTafarian, you’re probably asking yourself “so what?” since RAILS is a very productive framework. We’ll let’s go through what would have to change to see if it has any interest to you.
Let’s start with the fundamental problem – Rails equates each URI with a single action as opposed to a single resource that can be manipulated via HTTP verbs. For example, let’s say we have a products table with an id field and a name field. Then let’s run the rails scaffold generator. If you look at the generated code, you’ll see its creates a number of public methods that manipulate several resources. More concretely, the following table shows how the generated methods map to resources
and HTTP verbs.
Mapping RAILS to REST
Thus, I’ve identified four types of resources:
- The resource that represents the collection
of all products
- Any number of resources that identify individual products
- A creator resource
- An editor resource
The creator and editor resources might be confusing. An analogy I’ve heard before that helps me is to imagine your resource is a Microsoft Word document. Does that mean that Microsoft Word itself is part of your resource? No, that doesn’t make sense. Instead, Microsoft Word is a resource that you can use to manipulate your document resource. Thus, the creator and editor resources provide a representation (i.e., an HTML document) that allows you to create new product resources or edit existing product resources.
So how would these map to URIs. Well, that’s pretty easy:
http://myrailsapp/product http://myrailsapp/product/member/:id http://myrailsapp/product/creator http://myrailsapp/product/editor/:id
Instead of the standard RAILS route of :controller/:action/:id we are now doing :controller/:resource/:id. In addition, sInce http://myrailsapp/product/member/:id is so common, I tend to shorten it using routes to:
Still with me? If so, then is it possible to fit these ideas into RAILS
which did not start with REST in mind? Well, I know of at least two previous attempts,
both which provided inspiration but in the end aren’t satisfactory. I think
this third attempt falls into the same camp, but hopefully will keep
the discussion going and throw another idea or two into the pot.
So here’s my take – feel free to download this plugin if
you’d like experiment with live code:
class ProductController < ApplicationController include RestController def get @product_pages, @products = paginate :products, :per_page => 10 end def post @product = Product.new(params[:product]) begin @product.save! flash[:notice] = 'Product was successfully created.' redirect_to :resource => :collection rescue => e flash[:product] = @product redirect_to :resource => :creator end end resource :Member do def get @product = Product.find(params[:id]) end def put @product = Product.find(params[:id]) begin @product.update_attributes(params[:product]) flash[:notice] = 'Product was successfully updated.' redirect_to :id => @product rescue => e # Send the current invalid values to the editor via the flash flash[:product] = @product redirect_to :resource => :editor, :id => @product end end def delete Product.find(params[:id]).destroy redirect_to :id => nil, :resource => nil end end resource :Creator do def get @product = flash[:product] || Product.new end end resource :Editor do def get @product = flash[:product] || Product.find(params[:id]) end end end
Some things to notice about the code.
First, the REST support is added in via a mixin.
Second, we assume the controller itself represents the product collection resource and therefore define the get and post methods directly on it.
Third, other resources are demarcated by the resource keyword. Each resource can expose up to four methods – get, post, put and delete. However, in general only a subset of these methods makes sense for a given resource.
So how does this work under the hood? First, the resource keyword creates a Ruby module on the fly and includes it in the ProductController at compile time. As part of the merging process, the resource methods are renamed to include the resource name. Thus, the get method for the member resource becomes get_member. This is not ideal, but is needed to avoid name conflicts when multiple resources are included in a controller.
I also played with picking the correct resource at runtime and then merging
the resource into an instance of the productsController using Ruby’s extend
mechanism. That turned out to be fairly difficult to accomplish since
it interfered with the way that RAILS combines multiple modules into the
Base class in ActiveController (in particular I had problems with the flash). I also wondered about the performance implications, but didn’t do any testing
to see if there really were any.
Now let me tell you what I don’t like about the solution. The main issue is the method renaming. You have to know about it since you need to create templates called get.rhtml, get_memeber.rhtml, etc. It also comes into play if you want to turn on or off filters.
The second issue, umm….cough…., is that a pure REST solution does not work with HTML forms since browsers don’t support PUT and DELETE. You can get around this by using XmlHttpRequest which doe support PUT and DELETE. If you need to use PUT and DELETE in forms, then sadly you’ll have to add an action parameter to the URL like this – “action=put.” For those of you are annoyed about this like I am, first write your favorite browser maker a letter and then let the W3C have a piece of your mind since amazingly enough the HTML specification does not even mention the use of PUT and DELETE on forms. Hopefully, the WHATWG forum will solve this
issue in the future.
The last obvious issue you may have caught is look closely at the top level post method (or put). If the post fails we have to store the ill-formed product into the flash and redirect back to the editor since its at a different URL.