What's New in Edge Rails: Default RESTful Rendering
This feature is schedule for: Rails v3.0
A few days ago I wrote about the new respond_with functionality of Rails 3. It’s basically a clean way to specify the resource to send back in response to a RESTful request. This works wonders for successful :xml and :json requests where the default response is to send back the serialized form of the resource, but still presents a lot of cruft when handling user-invoked :html requests (i.e. ‘navigational’ requests) and requests where error handling is required. For instance, consider your standard create action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class UsersController < ApplicationController::Base respond_to :html, :xml, :json def create @user = User.new(params[:user]) # Have to always override the html format to properly # handle the redirect if @user.save flash[:notice] = "User was created successfully." respond_with(@user, :status => :created, :location => @user) do |format| format.html { redirect_to @user } end # Have to send back the errors collection if they exist for xml, json and # redirect back to new for html. else respond_with(@user.errors, :status => :unprocessable_entity) do |format| format.html { render :action => :new } end end end end |
Even with the heavy lifting of respond_with you can see that there’s still a lot of plumbing left for you to do – plumbing that is mostly the same for all RESTful requests. Well José and the Rails team have a solution to this and have introduced controller responders.
Controller Responders
Controller responders handle the chore of matching the HTTP request method and the resource format type to determine what type of response should be sent. And since REST is so well-defined it’s very easy to establish a default responder to handle the basics.
Here’s what a controller utilizing responder support (now baked into respond_with) looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class UsersController < ApplicationController::Base respond_to :html, :xml, :json def index respond_with(@users = User.all) end def new respond_with(@user = User.new) end def create respond_with(@user = User.create(params[:user])) end def edit respond_with(@user = User.find(params[:id])) end def update @user = User.find(params[:id]) @user.update_attributes(params[:user]) respond_with(@user) end end |
The built-in responder performs the following logic for each action:
- If the
:htmlformat was requested:- If it was a
GETrequest, invokerender(which will display the view template for the current action) - If it was a
POSTrequest and the resource has validation errors,render:new(so the user can fix their errors) - If it was a
PUTrequest and the resource has validation errors,render:edit(so the user can fix their errors) - Else, redirect to the resource location (i.e.
user_url)
- If it was a
- If another format was requested, (i.e.
:xmlor:json)- If it was a
GETrequest, invoke the:to_formatmethod on the resource and send that back - If the resource has validation errors, send back the errors in the requested format with the
:unprocessable_entitystatus code - If it was a
POSTrequest, invoke the:to_formatmethod on the resource and send that back with the:createdstatus and the:locationof the new created resource - Else, send back the
:okresponse with no body
- If it was a
Wading through this logic tree you can see that the default logic for each RESTful action is appropriately handled, letting your controller actions focus exclusively on resource retrieval and modification. And with that cruft out of the way your controllers will start to look even more similar – I suspect we’ll be seeing a solution for this coming around the bend shortly as well…?
So, just to recap the basics, here are a few action implementations side by side (the first being before responders and the latter being after):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Old def index @users = User.all respond_to do |format| format.html format.xml { render :xml => @users } format.json { render :json => @users } end end # New def index respond_with(@users = User.all) end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# Old def create @user = User.new(params[:user]) if @user.save flash[:notice] = "User successfully created" respond_to do |format| format.html { redirect_to @user } format.xml { render :xml => @user, :status => :created, :location => user_url(@user) } format.json { render :json => @users, :status => :created, :location => user_url(@user) } end else respond_to do |format| format.html { render :new } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } format.json { render :json => @user.errors, :status => :unprocessable_entity } end end end # New def create @user = User.new(params[:user]) flash[:notice] = "User successfully created" if @user.save respond_with(@user) end |
Oh yeah, that’s getting real lean.
Overriding Default Behavior
If you need to override the default behavior of a particular format you can do so by passing a block to respond_with (as I wrote about in the original article):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class UsersController < ApplicationController::Base respond_to :html, :xml, :json # Override html format since we want to redirect to the collections page # instead of the user page. def create @user = User.new(params[:user]) flash[:notice] = "User successfully created" if @user.save respond_with(@user) do |format| format.html { redirect_to users_url } end end end |
Nested Resources
It’s quite common to operate on resources within a nested resource graph (though I prefer to go one level deep, at most). For such cases you need to let respond_with know of the object hierarchy (using the same parameters as polymorphic_url):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UsersController < ApplicationController::Base respond_to :html, :xml, :json # In this case, users exist within a company def create @company = Company.find(params[:company_id]) @user = @company.users.build(params[:user]) flash[:notice] = "User successfully created" if @user.save # Ensure that the new user location is nested within @company, # for html format (/companies/1/users/2.html) as well as # resource formats (/companies/1/users/2) respond_with(@company, @user) end end |
If you have a singleton resource within your resource graph just use a symbol instead of an actual object instance. So to get /admin/users/1 you would invoke respond_with(:admin, @user).
Custom Responders
While there’s no facility to provide your own responder classes, it will no doubt be added shortly. If you look at the current responder class definition, it’s a very simple API essentially only requiring a call method (more intuitively take a look at the :to_html and :to_format methods).
Stay tuned here for further refinements to this very handy functionality – you’re going to see a lot more tightening in the coming weeks.
tags: ruby,
rubyonrails


Recent comments
1 year 23 weeks ago
1 year 23 weeks ago
1 year 25 weeks ago
1 year 27 weeks ago
1 year 42 weeks ago
1 year 45 weeks ago
1 year 45 weeks ago
1 year 45 weeks ago
1 year 46 weeks ago
1 year 48 weeks ago