views:

917

answers:

3

After reading the book RESTful Web Services by Leonard Richardson and Sam Ruby, it seems to me, that the to_xml method of rails is not as restful as it should be. Specifically, the book introduces the Resource Oriented Architecture, one of the tenets of which is connectedness: Representations of a resource should contain not only the data of the resource, but also links to other resources.

However, when rails scaffolds a resource, it implements requests for a xml representation by deferring to [model]#to_xml. This method does not have access to the normal path helpers, so any links to other resources are only indicated by their ids, not by their uris.

I have solved this problem for now, but the solution doesn't seem very robust: Given a Employer resource with nested Employees, the following code (sort of) adds uris to their xml serialization:

class Employee < ActiveRecord::Base

  include ActionController::UrlWriter

  belongs_to :employer

  def to_xml(options = {})
    options[:procs] = [ Proc.new {|options| options[:builder].tag!('uri', employer_employee_path(employer, self)) } ]
    if options[:depth].nil?
      options[:depth] = 1
    end
    if options[:depth] != 0
      options[:depth] -= 1;
      options[:include] = [:employer]
    end
    super(options)
  end
end

class Employer < ActiveRecord::Base

  include ActionController::UrlWriter

  has_many :employees

  def to_xml(options = {})
    options[:procs] = [ Proc.new {|options| options[:builder].tag!('uri', employer_path(self)) } ]
    if options[:depth].nil?
      options[:depth] = 1
    end
    if options[:depth] != 0
      options[:depth] -= 1;
      options[:include] = [:employees]
    end
    super(options)
  end
end

The UrlWriter allows me to properly create the path of a resource (not the full uri, though. The domain will have to be pasted on to the path by the clients of my web service). Now the models are responsible for their own uri, and for including the representation of any connected resource. I use the :depth option to avoid recursing endlessly.

This method works, but as stated before, it doesn't seem quite right, with all the duplication. Has anybody else had the same problem and does anybody else have a better idea of how to get uris in the xml representation?

+2  A: 

You could use the :methods option to include additional values. For example.

# in controller
employer.to_xml(:include => :employees, :methods => [:uri])

class Employee < ActiveRecord::Base
  include ActionController::UrlWriter
  belongs_to :employer

  def uri(options = {})
    polymorphic_path([employer, self])
  end
end

class Employer < ActiveRecord::Base
  include ActionController::UrlWriter
  has_many :employees

  def uri(options = {})
    polymorphic_path(self)
  end
end

Notice I'm using polymorphic_path here. This is a more generic path method which you might find more abstractions through.

However, I do not think this is that great of a solution. Including UrlWriter in the model is messy and can take a long time if you have a large number of routes. An alternative is to make the "uri" a simple accessor method.

# in model
attr_accessor :uri

And then you can set the uri for the model(s) in the controller. Unfortunately that is pretty messy too.

employer.uri = polymorphic_path(employer)
employer.employees.each { |e| e.uri = polymorphic_path([employer, e]) }
employer.to_xml(:include => :employees, :methods => [:uri])
ryanb
I agree, that including UrlWriter in the model is messy. Even the UrlWriter namespace indicates, that it is logically a part of the controller, not the model. I guess the overarching problem is that in the case of xml, the model is left in charge of its own representation.
Boris
Exactly. I think the XML presentation belongs in the view layer which makes sense if it needs to include URL paths etc. You could probably make a generic render_xml method which behaves like to_xml but is run in the scope of a view so it has access to all helpers.
ryanb
BTW, if you do end up calling "include ActionController::UrlWriter" in every model, its best to call it in ActiveRecord::Base just once. Startup times will be much better if there are a lot of routes (but, still I don't recommend this in general).
ryanb
A: 

Yes... this belongs in the view. Why not use the built in XML Builder?

#in controller
respond_to do |format|
  format.xml  # show.xml.builder
end

Then simply create a show.xml.builder file like so

# in show.xml.builder
xml.instruct!
xml.employer do
  xml.name @employer.name
  ...
  xml.uri polymorphic_path(employer)
end
Mike
A: 

Putting a bunch of Builder code in your controller is kind of unsatisfying to me. Plus then you can't use Array#to_xml, which is pretty concise and convenient.

How about something like this:

#things_controller.rb 
def index
  render :xml=>Thing.all.to_xml(:thing_url=>thing_url('#{self.code}'))
end

#thing.rb
def to_xml(options={})
  if thing_url = options[:thing_url]
  thing_url = eval('"'+CGI.unescape(options.delete(:thing_url))+'"') 
  super do |builder|
    builder.tag!('thing-url', thing_url) if thing_url
  end
end
substars