REST on Rails
November 2, 2005
Imagine a news website with simple URLs for its news items and the categories they
belong
to: http://example.com/item/15
and
http://example.com/category/rails
. Suppose that it has been built using Ruby
on Rails. In the article, we'll discuss how to REST-enable this site, giving read-write
HTTP
access to machine-readable representations of its resources. By the end of the article,
we'll be able to add these features to our site in just a couple of lines of code.
If you're not yet familiar with the Rails framework, I suggest you introduce yourself to it via an article like "What Is Ruby on Rails" before reading on. You won't regret it. For more background on the REST architectural style, Joe Gregorio's series of XML.com articles "The Restful Web" are very useful.
(rest_resource.rb contains all the code excerpted in this article.)
Mapping REST Concepts to Rails Patterns
Following the Model/View/Controller pattern, our site's resources will be represented by Model classes, in this case, an Item and a Category class. In REST terminology, these are resources, URL-addressable entities that you can interact with over HTTP. Views are a way to create REST's representations, useful serializations of the models that clients can interpret. We decide which representation of which resource to use in dispatch, a role played by the Controller.
HTTP's standard PUT/GET/POST/DELETE verbs translate directly to the Create/Read/Update/Delete features built into every Rails ActiveRecord model. This means no changes or additions for existing model classes to be REST-enabled -- as is usual in the MVC framework, models don't have to know or care what they're being used for.
We'll provide a view that does an automatic mapping from any Rails model to a default XML representation. We'll build it with a template using ActiveRecord introspection to discover the schema and record values at runtime. The XML format will look something like this:
<nameofmodelclass> <field1>field 1's value</field1> <field2>field 2's value</field2> <onetomanyfield1 href='http://example.comm/url/of/relatedrecord1' /> <onetomanyfield1 href='http://example.comm/url/of/relatedrecord2' /> </nameofmodelclass>
When a client POSTs such an XML document to the URL of an existing resource, we'll
parse it
and update the values of the record in the database. Clients will also be able to
create new
resources by POSTing XML to a create
URL and being redirected to the newly
created resource. This logic will be implemented in the Controller.
Dispatch with ActionController
To handle incoming requests to create, read, update or delete resources, we need to inspect the incoming request before we can take action. Rails handles most of the hard work in decoding a URL and calling the right controller method, but it doesn't differentiate between different HTTP verbs. We'll have to make this choice in our controller method. ActionController provides a bunch of convenient Boolean methods for this using different HTTP verbs, so the skeleton of our code looks like this:
if request.post? if params[:id] # it's an update else # no ID supplied, so create new resource end end if request.get? # serve up a representation end if request.delete? # delete the resource end
You may like to compare this dispatch mechanism to the Python code in Joe Gregorio's Dispatching in a REST Protocol Application.
Using REXML and Builder
To implement the XML handling for this application, we're going to need a way of consuming XML, and a way of producing it. Fortunately every Rails installation ships with REXML and Builder.
Builder is the standard Rails mechanism for producing XML. If you're familiar with
.rhtml
files used for HTML templating then making the requisite
.rxml
files won't be a great leap, and will integrate with your application's
controllers in just the same way. Here's a simple example of producing some XML:
xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8" xml.example('version' => '2.0') do xml.foo("This is the title") end
The template produces this output:
<?xml version="1.0" encoding="UTF-8"?> <example> <foo>This is the title</foo> </example>
Ruby ships with REXML, which incorporates both a parser and an XPath implementation. You can parse an XML document like this:
doc = REXML::Document.new(resp.body)
and extract data with XPath like so:
REXML::XPath.each(doc,"/example/foo") { |xml| puts xml.text }
Introspection with ActiveRecord
Let's take a break from XML and look at how we can dynamically get data from any model
object without prior knowledge of its fields. To understand fully how this works,
I highly
recommend firing up the Rails console (run script/console
from the command
line) on any Rails project and trying out the commands interactively. We'll show excerpts
from an interactive session as we go through the concepts.
Every model is a subclass of ActiveRecord::Base, and this brings with it a number
of
essential methods for our task. The first is content_columns
:
>> i = Item.find_first() => #<Item:0x2333814 @attributes={"title"=>"This is an article's title", "id"=>"1", "bodytext"=>"The text of the item"}> >> i.class.content_columns => [#<ActiveRecord::ConnectionAdapters::Column:0x2331bcc @type=:string, @default=nil, @limit=100, @name="title">, #<ActiveRecord::ConnectionAdapters::Column:0x2331adc @type=:string, @default=nil, @limit=250, @name="bodytext"> >> i['title'] => "This is an article's title"
Notice that content_columns
is a class method, so we invoke it as
i.class.content_columns
. It tells us that Items have two content columns of
type string
. We can loop through this list of column names in our XML template
to get their values.
What if the model has associations? ActiveRecord has powerful automatic handling of database joins to manage relationships between models. These relationships are a key part of most Rails apps. ActiveRecord provides another useful class method:
>> i.class.reflect_on_all_associations => [#<ActiveRecord::Reflection::AssociationReflection:0x2337860 @macro=:belongs_to, @options={}, @name=:category, @active_record=Category>]
This tells us that our Item has one association: it belongs to a Category. To find out what category our item belongs to, we just use one line of Ruby to resolve and call the category method given there, and we have all the data we need to make a serialization:
>> i.method('category').call => #<Category:0x22fb2e8 @attributes={"description"=>"Technical articles", "id"=>"2" }>
Putting it all together, the RXML template looks like this:
xml.tag!(@obj.class.to_s.downcase,{:id => @obj.id}) { @obj.class.content_columns.each { |col| xml.tag!(col.name,@obj[col.name]) } @obj.class.reflect_on_all_associations.each { |assoc| if assoc.macro == :belongs_to || assoc.macro == :has_one rels = [@obj.method(assoc.name).call] end if assoc.macro == :has_many || assoc.macro == :has_and_belongs_to_many # *_many methods return lists rels = @obj.method(assoc.name).call end rels.each { |rel| if rel name = rel.class.to_s.downcase xml.tag!(name,{:id=>rel.id, :href =>url_for(:only_path=>false,:action => name+"_xml",:id=>rel.id)}) end } } }
Using this template and a little REXML XPath code as above, we can complete the code for dispatch in the controller.
In Action
Let's see a command-line session with our REST-enabled web app. We'll use the REST
hacker's
number one web services client: /usr/bin/curl
.
First create a new Item by POSTing some XML to the app:
$ curl -i -X POST -d "<item><title>Article title</title><bodytext>no body</bodytext></item>" http://localhost:3000/blog/item HTTP/1.1 201 Created Date: Sun, 09 Oct 2005 13:12:54 GMT Location: http://localhost:3000/blog/item/1137975
The server parsed our XML, created an Item model instance, initialised it with our
data and
stored it in the database. It indicated success with the HTTP code 201 Created
,
and told us where to find the new resource with the Location header.
Now request a representation of the resource from its newly-minted URL:
$ curl http://localhost:3000/blog/item/1137975 <item id="1137975"> <title>Article title</title> <bodytext>no body</bodytext> </item>
The XML comes back as expected. Make a change to the resource by posting data to its URL:
$ curl -i -X POST -d "<item><title>New title</title></item>" http://localhost:3000/blog/item/1137975 HTTP/1.1 200 OK Date: Sun, 09 Oct 2005 13:14:29 GMT Content-Type: text/xml <item id="1137975"> <title>New title</title> <bodytext>no body</bodytext> </item>
Note that the title has changed but the bodytext
field remains untouched as we
didn't specify it in our input.
Finally, delete the resource:
$ curl -i -X DELETE http://localhost:3000/blog/item/1137975 HTTP/1.1 204 Deleted Date: Sun, 09 Oct 2005 13:14:29 GMT Content-Type: text/xml
And verify that it's gone:
$ curl -i http://localhost:3000/blog/item/1137975 HTTP/1.1 404 Not Found Date: Sun, 09 Oct 2005 13:17:29 GMT
Let's Make it a One-Liner
Now we have our code written, wouldn't it be nice if we could apply it to any model
in any
rails project? Let's package up the code so that it can be pulled in with one line
of
controller code, just like Rails builtins such as belongs_to
,
validates_uniqueness_of
and layout
.
To do this, we exploit Ruby's dynamic nature by providing a mixin class that transparently adds features to an existing class. There's a fairly simple code pattern for doing this:
module ExtraMethods def self.append_features(base) super base.extend(ClassMethods) end module ClassMethods def additional_method(message) module_eval <<-"end_eval",__FILE__,__LINE__ def example puts "I've been added to give you this message: '#{message}'" end end_eval end end end
We can now use this in any class with a call to include
:
class SomeClass include ExtraMethods additional_method "your dynamic method insertion worked" end >> SomeClass.new().example() I've been added to give you this message: 'your dynamic method insertion worked
You can see this Ruby pattern in use in the code that accompanies this article. With
the
final code present in the lib
directory of the project, a REST-enabled
controller looks like this:
class NewsController << ApplicationController rest_resource :item rest_resource :category end
Conclusion
Rails is a strong foundation for the REST style. Its approach to database modeling and URL routing leads to good URLs representing resources. These are easy to enable for XML reading and writing using simple dispatch mechanisms. Ruby's dynamic language features make it possible to package up convenience code into an easy-to-use one-liner.
In any application where the client can make changes to your database, you have to think carefully about granting access rights. The code presented here could be enhanced to use a Rails-based authentication system, or alternatively a web server such as Apache can be used to front an application and grant fine-grained rights dependent on user and HTTP verb.
It's instructive to compare the style of interaction in this article with the emerging Atom API standard, and to think about how this kind of API lends itself well to embedding in all sorts of clients, from command utilities to OSX Dashboard widgets, to Web 2.0 mashups.