Rails’ baked-in REST support is great. Build your app right, and you can expose a programmatic interface to your users for free.
That said, many times providing views in non-HTML formats tends to be bulky and unwieldy. You end up with either very brittle representations of your data, or extremely bulky respond_to blocks in your controllers.
Fortunately, there’s a better way! We’re going to provide two new render targets, :to_yaml and :to_json which will let us write a single XML builder view, and then provide that view in XML, YAML, and JSON formats according to the consuming developer’s preferences.
In application.rb you’ll want to override the render method.
def render(opts = {}, &block)
if opts[:to_yaml] then
headers["Content-Type"] = "text/plain;"
render :text => Hash.from_xml(render_to_string(:template => opts[:to_yaml], :layout => false)).to_yaml, :layout => false
elsif opts[:to_json] then
content = Hash.from_xml(render_to_string(:template => opts[:to_json], :layout => false)).to_json
cbparam = params[:callback] || params[:jsonp]
content = "#{cbparam}(#{content})" unless cbparam.blank?
render :json => content, :layout => false
else
super opts, &block
end
end
As you can see, we render a single XML view, and then load it to a hash from XML, and use Rails’ built-in Hash#to_json and Hash#to_yaml methods to provide the data in the desired format. There is a single glaring problem with this approach, though - Hash#from_xml is dog slow because it uses REXML. There’s a fantastic solution, though!
Courtesy of a blog post over at cobravsmongoose, we have a libxml drop-in for Hash#from_xml
First, install libxml and then faster_xml_simple.
Second, include a monkeypatch to Hash#from_xml with the following:
require 'faster_xml_simple'
class Hash
def self.from_xml(xml)
undasherize_keys(typecast_xml_value(FasterXmlSimple.xml_in(xml,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__')
))
end
end
You can run the benchmarks if you’d like, but it’s orders of magnitude faster than REXML. Seriously. Don’t use REXML. It’s like trying to run a Ferrari off of a 9-volt battery.
Now, let’s say you have an action you want to provide HTML, XML, JSON, and YAML views for.
def index
...
respond_to do |wants|
wants.html
wants.xml { render :layout => false }
wants.json { render :to_json => "posts/index.xml.builder" }
wants.yaml { render :to_yaml => "posts/index.xml.builder" }
end
end
Finally, throw together your index.xml.builder file as you best see fit.
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
xml.posts do
@posts.each do |post|
xml.post(:id => post.id) do
xml.user(:id => post.user.id) +
xml.content do
post.post_body
end
end
end
end
And all of a sudden, bam! You’ve got your posts available in HTML…
/posts/index
…and in XML, YAML, and JSON, along with the associated User. By using an XML builder, you can make the serialized data as complex and customized as you’d like. No more funky respond_to blocks, no more exposing data you don’t want to. Expose what you want, and just what you want, in several formats.
/posts/index.xml
/posts/index.yml
/posts/index.json
One final trick is that the JSON views accept an optional callback or jsonp parameter, which will cause the content to be passed to a Javascript function matching the passed parameter, as per the JSONP spec.
For example, if you have a /foo/bar.json view that would render the following JSON:
"{\"foo\":\"bar\"}"
Calling /foo/bar.json?jsonp=returnFunc would return the following:
returnFunc("{\"foo\":\"bar\"}")
Check out the JSONP spec for more.
12 Comments
I think it would be cleaner to augment render to allow you to pass a render :format => :xml for the json and yaml responses and then to use an after_filter to convert the response to the correct format and mime-type.
Very interesting approach, Chris. That would help clean up the respond_to blocks, as well!
I think the guys at boomloop/playtype did a good write up on why the out-of-the-box rails approach is wrong for a few reasons: http://playtype.net/past/2008/5/20/rest_on_rails_filling_in/
I completely agree, Glenn. That was one of my primary reasons for this approach. I’d first tried to implement a fancy ActiveRecord plugin that defined a to_xml schema for models, but that turned out to be both cumbersome, and the Wrong Way(TM) to do it.
I’ve found this approach to be extremely flexible, letting me expose only data I want to, in the formats I want to, without requiring special gymnastics to make it work.
Excellent - I love this idea. My only minor quibble is that it’s a little confusing having an xml builder in the JSON renderer. I would prefer an abstract structure builder (index.structure.builder) that would be semantically clearer.
I really think this is overcomplicating a simple problem.
For JSON and YAML, all you need to do is define a method on the model you want to expose, like so:
Of course, if you want, you define a method like so:
Then you can have:
That means no code in the controller… much cleaner.
Also provides you a little hook to even add computed attributes if you like, or include even a user’s attributes instead of a user ID. Whatever.
Much cleaner in my opinion.
Sorry for the lack of format… I don’t really know what formatting options there are with WordPress.
My issue with tying to_json/to_yaml to the model is that it presumes that I will always want the same representation of the model everywhere I want to express that model in a serialized format. It’s a fairly brittle solution, though certainly a quick and easy one. I found that often times, I needed to have a lot of control over the data that was being exposed in these formats, and doing so required jumping through all kinds of hoops, either in the controller or in the model, when what I was really doing was trying to express a specific view for the model.
In the MVC architecture, the model should simply present its data to the view, and the view should decide how to present that data to the user; placing ownership of the model’s displayed format in the model’s hands violates MVC, and makes using it somewhat more brittle.
But here you do end up with misrepresentation and a bit more explicit definition where it’s not exactly necessary. So you want multiple attribute profiles (for a lack of a better term)? Just have different methods that respond with hashes with the right data…
Of course, you’d abstract it out to something where you can just name which parameters to expose, and have it always respond with a Hash. This way calling #to_json or #to_yaml on it always gets what you want.
There may be better solutions but I think putting attribute publicity is something that the model should concern itself with, especially when you can keep the view very simple and clean (and not requiring transforming to XML then back again). It makes changes in the model site-wide and maintenance of those views not necessary.
The abstracted method that lets me pass a list of desired attributes and get back a hash is the method I’d originally gone with in a recent app…until I realized that when I wanted to construct complex views of the data, I was passing in massive multi-level hashes that defined a hierarchy of desired attributes across a series of models that were effectively doing the same thing that an XML builder did for me.
In simple cases, some kind of “request attributes X, Y, and Z, get a hash back” on the model works great. What I’ve found is that by allowing each view to decide what data is relevant, I can expose more or less data for each API call, with the intent of reducing the number of followup calls that a developer would have to make in order to get all the data they want, without necessarily bloating the response results by throwing in everything and the kitchen sink.
If I have topics -> posts -> users, and I request a list of a user’s posts in a serialized format, I may want something like:
However, if I want to get a list of posts in a thread, the view may need to be something like:
Two very different views. To do those in the model, I’d need something like
Oof. Starting to get a little heavy, and that’s both assuming a for_serialization that accepts nested attributes like that, and a relatively simple example.
If you want to get more complex and start including data that’s not necessarily available to the model, like, say, links generated via your routing, then you have to start passing procs into your for_serialization. It becomes a giant unholy mess.
I did initially go the route of an ActiveRecord extension which provided super-customizable serialization calls, with nested associations and procs and the like, and it was just incredibly unwieldy to use in practicality.
Hello webmaster
I would like to share with you a link to your site
write me here preonrelt@mail.ru
It took me two hours after reading your article to get JSONP to work, I was missing adding an explicit :callback parameter!
format.json { render :json => @artists.to_json(), :callback => params[:callback] }
3 Trackbacks
[...] Coffee Powered » Powerful, easy, DRY, multi-format REST APIs [...]
From vBharat.com » Powerful, easy, DRY, multi-format REST APIs…
Rails’ baked-in REST support is great. Build your app right, and you can expose a programmatic interface to your users for free….
[...] means that the faster_xml_simple monkeypatch is no longer needed. I don’t think we’re doing much else with XML on blippr, but [...]