Thursday, August 30, 2007

Rails: Resource Links in Resource List

Navigating a REST application should not require a deep understanding of the namespace of the application's addressable resources. As much as possible, one REST query should lead to another.

The most obvious case of this is when one has a resource that is a list or collection (e.g. /customers) of the available resources (e.g. /customers/1), e.g.:


<customers>
<customer>
<name>The Customer</name>
<a href="myserver/customers/1.xml" />
</customer>
</customers>


And yet, it's surprisingly difficult to put these kind of links into Rails resources, particularly if you use the approach that resource_scaffold suggests.

Resource scaffold generates the XML representation of the collection as by calling the to_xml method that Rails adds to Array, which in turn calls to_xml on the model classes, something like this:

@customers = Customers.find(:all)
render :xml => @customers.to_xml


In order to add links to the XML representation, you'd need two pieces of information: the URL structure for resource and the id of the resource. The ActionController has the most understanding of the URL structure (as it can use
url_for and named resource helpers like customer_path), but the model class knows its id and is responsible for generating the XML.

This awkward split in the available information makes adding these links to the XML somewhat painful; perhaps no more painful than building the XML by hand would normally be, but it's a departure from the usual ease of accomplishing common tasks in Rails.

After trying a number of alternatives, we settled on approach. In the controller, we do:

# GET /customers.xml
def index
@customers = Customer.find(:all)
respond_to do |format|
format.xml do
render :xml => @customers.to_xml( :format => :summary, :base=>url_for(:controller=>'customers') )
end
end
end

# GET /customers/1.xml
def show
@customer = Customer.find(params[:id])
respond_to do |format|
format.xml { render :xml => @customer.to_xml( :format => :full ) }
end
rescue ActiveRecord::RecordNotFound
head 404
end


This relies on some model code, like this:

def to_xml( options = {} )
case options[:format]
when :summary
resource_url = proc { |options| options[:builder].a(:href=>"#{options[:base]}/#{id}.xml") }
super( options.merge!( :only => [:id, :name], :procs => [resource_url] ) )
when :full
super( options.merge!( :include => [ :main_contact_info, :billing_contact_info ],
:except => [ :main_contact_id, :billing_contact_id ] ) )
else
super( options )
end
end


It's still a little awkward, and I'm hopeful that we'll find a better alternative, but it does the job and is less ugly than some of the other alternatives we considered.

The only other option we looked at that seems reasonable is to generate all the XML within the controller, which means reproducing the work done by Array.to_xml, but is otherwise relatively sensible.

2 comments:

dkubb said...

Check out the Presenter Pattern, and specifically the restfully_yours plugin's Presenter class.

The Presenter class allows you to define to_xml (or to_*) methods in one place, outside of your controllers and models. You use the presenter in your controller and it knows all about controller's state, so it can use url_for to generate the URLs.

Geoffrey Wiseman said...

After trying other approaches, I eventually decided that I wanted to control the XML representation more than to_xml was really meant; I started using RXML (xml builder, although erb/xml woulda been fine as well) templates.