Rest With Rails Part 2 : Serving XML, JSON and Atom
Join the DZone community and get the full member experience.
Join For FreeIn part 1 of REST with Rails we had an introduction to creating RestFull services with Rails. In this article we will be looking into serving this content using different representations including XML, JSON and Atom.
Every resource has a representation, in fact, a given resource can have more than one representation. Users accessing our task manager will want to see an HTML page listing all their tasks, or they may choose to use a feed reader to subscribe to their task list; feed readers expect an Atom or RSS document. If we're writing an application we would want to see the tasks list as an XML document, or JSON object, or perhaps pull it into a calendar application in the form of an iCal list of todo and events. In this section we're going to explore resources by looking at multiple representations, starting with HTML and adding XML, JSON and Atom representations for our tasks list.
This article is based on chapter 5 from Ruby in Practice by Jeremy McAnally and Assaf Arkin. Courtesy of Manning Publications. All rights reserved.
[img_assist|nid=4838|title=|desc=|link=url|url=http://www.manning.com/mcanally/|align=right|width=139|height=174]
PROBLEM
As we're building our task manager we realize the need to support a number of clients, specifically feed readers, and programmable clients, by adding XML, JSON and Atom representations to the tasks list.
SOLUTION
One reason we recommend Rails for building Web services is the ease of adding different representations for the same underlying resource. So let's start with a simple action that displays the current task list in one of several formats:
def index
@tasks = Task.for_user(@user_id)
end
Since most Rails examples look like this and only support HTML, we won't fault you for thinking this example only shows an HTML output, but in fact it supports as many formats as the views we've got. When you leave it up to Rails to render the response, it tries to find a suitable view based on the action name and expected format. If we wrote a view called index.html.erb, Rails will use it to render HTML responses. If we add a view called index.xml.builder, Rails will use this one to render XML responses. For Atom, we would use index.atom.builder and for iCal, index.ics.erb.
Notice the pattern here? The first part tells Rails which action this view represents, the second part tells it which format it applies to, and the last part tells it which templating engine to use. Rails comes with three of these: ERB (eRuby), Builder and RJS. This is a new feature introduced in Rails 2.0. Earlier versions were less flexible, and always match a combination of format and templating engine, so for HTML it will default to ERB by looking up the view index.rhtml, and for XML it will default to Builder by looking up the view index.rxml. Rails 2.0 gives you more flexibility in mixing and matching formats and templating engines, and also makes it easier to add new template handlers, for example for using Liquid templates or HAML.
In a moment we're going to show you Builder when we use it to create an Atom feed for our tasks list. For XML and JSON we're not going to go through the trouble of creating and maintaining a custom view, instead we'll let ActiveRecord do a trivial transformation of our records into an XML document or a JSON object:
def index
@tasks = Task.for_user(@user_id)
case request.format
when Mime::XML
response.content_type = Mime::XML
render :text=>@tasks.to_xml
when Mime::JSON
response.content_type = Mime::JSON
render :text=>@tasks.to_json
when Mime::HTML, Mime::ATOM
# Let Rails find the view and render it.
else
# Unsupported content format: 406
head :not_acceptable
end
end
That happens to be the long way to do things. You can see the short way to respond with different content types in listing 4.
def index
@tasks = Task.for_user(@user_id)
respond_to do |format|
format.html
format.xml { render :xml=>@tasks }
format.json { render :json=>@tasks }
format.atom #5
end
end
We're using the respond_to method to match each format we support and the logic to render it. It's similar to the case statement above, but simpler to specify and more declarative. We're also letting the render method do all the hard work by asking it to convert the array into an XML document or JSON object and set the Content-Type header appropriately. Shorter to write, easier to maintain. Now it's time to handle the Atom view, for which we'll create a view file called index.atom.builder:
atom_feed do |feed|
feed.title "My tasks list"
feed.updated @tasks.first.created_at
@tasks.each do |task|
feed.entry task do |entry|
entry.title task.title
entry.content task.description, :type => 'html'
end
end
end
The call to atom_feed creates an XML document with the right wrapping for a feed, including the XML document type declaration, feed element with ID, and alternate link back to our site. It also creates an AtomFeedBuilder object and yields to the block. From the block, we're going to create the feed title, specify the last update, and add all the feed entries.
We now have a tasks resource that responds to GET and returns the task list in four different content type, HTML for Web browser, Atom for feed readers, and either XML or JSON for client applications.
DISCUSSION
The HTTP protocol allows clients to request data in a particular format using content negotiation. When the client sends a request to the server, it uses the Accept header to indicate all the content types it supports in order of preference. The server can pick the most suitable content type and use it when responding to the client in a format the client understands. If the server doesn't support any of the listed content types, it simply responds with 406 (Not Acceptable). Another status code, 415 (Unsupported Media Type), tells the client that the server does not support the content-type of a POST or PUT request.
That's the basic idea behind content negotiation. In some cases it's clearly the right thing to do. We can use one resource URL and send it to all our clients, and each client can see a different representation of the same resource. A Web browser will see an HTML page, a feed reader will see an Atom feed, other applications may see XML or CSV.
Another approach uses different resource URLs for each representation. Some people prefer this approach since it allows you to manage different representations, for example, you can send someone a URL to an XML document. If you want to download a CSV document using a Web browser, you need a URL that will always sends back a CSV document.
There is no one true way to construct these URLs, but there are two common conventions. One convention adds a query parameter that indicates the expected content type, for example, you can decide to use the format query parameter, and use a URL like /tasks?format=xml. Another convention is to use an extension suffix on the URL path, for example, /tasks.xml. We recommend using the extension suffix, for the simple reason that saving the document with a Web browser will retain the suffix, and having a file called tasks.xml will always open in the right application.
How does Rails handle it? When we use the built-in mechanism to decide on the content type, as we did in this section, Rails picks up the expected format from the query parameter format, or if not there from the URL path suffix, or it not there from the Accept header. Which way you use to request different content types is up to you, a Rails application can support all three. You'll notice in the previous section, when we wrote an action to create a new task, we did this:
Task.create!(params[:task])
Multiple representations work both way. If we can create a response and send back an XML document, we better be able to process a request by accepting the same XML document. When Rails processes an XML request, it converts the XML document into a Hash, using the document element's name for the parameter name. The above example expects the document to contain the element <task> and passes the hash to ActiveRecord.
It works the same way for HTML forms, if you follow the simple naming convention guidelines set by Rails. In our forms, we will have fields like task[title] and task[priority]. Rails uses this naming convention to figure out how the fields relate to each other, and turn them into a Hash parameter, so we can use the same line of code to process an XML document or the submission of an HTML form. It helps that we're using Rails' form helper methods, for example:
<% form_for @task do |f| %>
<%= f.text_field :title %>
<%= f.text_field :priority %>
<% end %>
The form_for creates the <form> element, figures out the action URL, and takes care to map the field names from title to task[title]. Give it a new record and it will point the form to the URL for creating a new resource (tasks_url); give it an existing record and it will point the form to the URL for updating an existing resource (task_url(@task)). That probably explains why we used Task.new to render the form in the new action. Rails comes with built-in support for HTML forms, XML, JSON and YAML, and if that's not enough you can always add customer parameter parsers, have a look at ActionController::Base.param_parsers for more information.
In the example above we showed you how to use AtomFeedBuilder, a templating mechanism for generating Atom feeds. AtomFeedBuilder itself extends the more generic XML templating mechanism provided by Builder::XmlMarkup. Let’s take a moment to look at Builder and what you can do with it.
Builder is a simple templating mechanism for creating XML documents from Ruby code. Because it always produces well-formed documents, some developers even use it to generate XHTML pages. It's available as a Gem you can use in any application that needs to generate XML, and also included as part of Rails. Builder is very simple to understand and intuitive to use, and a good example for what can be done with a little bit of meta-programming.
When you call a method on a Builder object, it takes the method name and uses it to create an XML element with the same name. This is done through method_missing, there is no need to specify any of these methods in advance. AtomFeedBuilder only specifies a few methods that do a lot more than just generate an XML element, so it defines entry but doesn't bother to define title or content.
As you can imagine from this example, passing a string argument will use that value for the element content; a hash argument specifies the element's attributes; and blocks are used to nest one element within another. Besides these, there are some special methods you can call like tag! to create an element with a given name (for example, to handle special characters or namespaces), text!, cdata! and comment! (they do exactly what you think they would), and instruct! to create the XML declaration at the top of the document.
METHOD_MISSING AND BLANKSLATE
Builder is an interesting use of method_missing. Ruby's objects use method passing. When you call a method on an object, Ruby first tries to match it against a known method definition, and if it doesn't find any method, pass it on to the object's method_missing. The default implementation throws NoMethodError. Builder uses method_missing to catch method calls and convert them into XML elements, so we don't need to declare an XML Schema or build any skeleton objects to get this simple creation of XML documents from Ruby code.
Existing object methods may clash with XML element names, for example, names like id and type are commonly used as element names. To solve that, Builder uses BlankSlate, a class that has most of its standard methods removed (In Ruby 1.9 you can achieve the same using BasicObject).
We mentioned before that you can use different URLs for the various representations. When we defined the tasks resource, Rails created several named route methods like tasks_url and task_url. In addition, an we didn't show that before, it created named route methods that accept a format and return a URL that specifies that output format in the form of a path suffix. These method names start with formatted_ and accept an additional argument that specifies the output format, and will show up when you run the rake routes task. So let's add a link users can use to subscribe to the Atom feed, using a named route:
<%= link_to "Subscribe", formatted_tasks_url(:atom) %>
In this section we showed you how to build a RESTful Web service. What if you want to access that service from another application? In the next section we’ll talk about ActiveResource, Rail’s way of accessing remote resources using an ActiveRecord-like API.
Opinions expressed by DZone contributors are their own.
Trending
-
RBAC With API Gateway and Open Policy Agent (OPA)
-
Seven Steps To Deploy Kedro Pipelines on Amazon EMR
-
Tech Hiring: Trends, Predictions, and Strategies for Success
-
SRE vs. DevOps
Comments