That is very sweet. It is great to see more work on RESTful Rails. It seems to me that each attempt gets closer to an approach I could believe in and be proud of. And, I get a warm fuzzy feeling any time I see a domain specific language developing. The
resource handler Charlie has created is definitely part of a DSL there.
He brings up a few issue that result from his implementation. Some of which are important and some, IMHO, are not.
Charlie points out that the abstractions start to leak a little when you get to creating the views for a
The main issue is the method renaming. You have to know about it since you need to create templates called get.rhtml, get_member.rhtml, etc. It also comes into play if you want to turn on or off filters.
That is very unfortunate. The easiest way to solve this problem might be rethink what makes up a controller. The
RestController design seem intent on combining the functionally of a cluster of related resources. In the example he provides the
ProductController support interaction with the following resources
* every known product * the collection containing every known product * an editor for product resources * a creator for product resources
This set of resources are very cohesive and quite coupled to one another so combining them into a single controller is reasonable. But it causes this problem that you have know that the
RestController is going to take
resource :Member do def get #.. end end
and turn it into an action named
Perhaps it would be better to conceptualize a controller as a bit of code that mediates interaction with exactly one type of resource. With this view of the world you would end up with more, smaller, controllers. Charlie’s product example would look more like
class ProductController < ApplicationController include YarController # that's YetAnotherRestController verb :get do @product = Product.find(params[:id]) end verb :put do @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 verb :delete do Product.find(params[:id]).destroy redirect_to :id => nil, :resource => nil end end Class ProductsController < ApplicationController include YarContoller verb :get do @product_pages, @products = paginate :products, :per_page => 10 end verb :post do @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 => :editor end end end
And so on… The main benefits of this is that the template to rendered for a get of ‘http://mystore.example/product/243’ is ‘apps/view/product/get.rhtml’ and, I think, the issues with filters go away, too. The down side is that you end up with four controllers for each basic type of resource you expose. One for the basic resource type you want to expose, one for the collection of all of those basic resources, one for the creator resource and one for the editor resource. I don’t know if the extra boiler plate code is worth the benefits but it feels like it might be.
PUTs and DELETEs
Charlie also points out
a pure REST solution does not work with HTML forms since browsers don’t support PUT and DELETE
He is absolutely correct. However this tunneling PUT and DELETE over POST kludge does not bother me very much. I will now take a moment to revel in being more pragmatic than Charlie, quite possibly for the first time since I meet him seven years ago. Anyway, it is ugly that HTML does not support PUT and DELETE but still very workable.
Handling Bad Data
Finally there is an issue with handling failed attempts PUT/POST. This is the one that bothers me the most. It is not really all that bad from a pragmatic standpoint, storing this info in state works fine. However, it implies a certain weakness in my world view because I did not see it coming.
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.
The fundamental problem here is that the separate editor resource will PUT the modified resource when you click save/submit. But what if you messed it up and, say, violated the business rule that blue products must have a price that is divisible by three? In a normal Rails app that proposed change would fail validation and the update action would just re-render the edit page with bad fields highlighted. But in a RESTful world the editor and the validation code are separate and it is wrong from REST stand point to just render the editor resource from in response to a product resource request. However, if you don’t do that you need to get the form data, and which fields are bad, from the previous attempt so that you can re-render the editor with the information the user previously entered and what was wrong with it.
One way you could solve this problem is to allow the creation of “invalid” resources. For example, you require a product to have a description. However, you receive a POST to ‘http://mystore.example/products’ without a description. You could issue the product an ID and store it in it’s invalid state (without a description) and then redirect the browser to the editor resource for that newly created, but invalid, product. That feels really clean from a design stand point but I am not sure how difficult it would be to implement. And you would certainly end up to permanently invalid resources, which might be hard to manage in the future.