My Profile Photo

chris.heald.me


My personal blog. Software, devops, security, music, and general nerditry.


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.

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;"
    xml = render_to_string(:template => opts[:to_yaml], :layout => false)
    render :text => Hash.from_xml(xml).to_yaml, :layout => false
  elsif opts[:to_json] then
    xml = render_to_string(:template => opts[:to_json], :layout => false)
    content = Hash.from_xml(xml).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.

comments powered by Disqus