Chapter 15. XML and ActiveResource

 

Structure is nothing if it is all you got. Skeletons spook people if they try to walk around on their own. I really wonder why XML does not.

 
 --Erik Naggum

XML doesn’t get much respect from the Rails community. It’s “enterprisey.” In the Ruby world, that other markup language, YAML (Yet Another Markup Language), gets a heck of a lot more attention. However, use of XML is a fact of life for many applications, especially when it comes to interoperability with other systems. Luckily, Ruby on Rails gives us some pretty good functionality-related to XML.

This chapter examines how to both generate and parse XML in your Rails applications, starting with a thorough examination of the to_xml method that all objects have in Rails.

The to_xml Method

Sometimes you just want an XML representation of an object, and ActiveRecord models provide easy, automatic XML generation via the to_xml method. Let’s play with this method in the console and see what it can do.

I’ll fire up the console for my book-authoring sample application and find an ActiveRecord object to manipulate.

>> Book.find(:first)
=> #<Book:0x264ebf4 @attributes={"name"=>"Professional Ruby on Rails
Developer's Guide", "uri"=>nil, "updated_at"=>2007-07-02T13:58:19-
05:00, "text"=>nil, "created_by"=>nil, "type"=>"Book", "id"=>"1",
"updated_by"=>nil, "version"=>nil, "parent_id"=>nil, "position"=>nil,
"state"=>nil, "created_at"=>2007-07-02T13:58:19-05:00}>

There we go, a Book instance. Let’s see that instance as its generic XML representation.

>> Book.find(:first).to_xml
=> "<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at
type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by
type="integer">
  </created-by>
  <id type="integer">
1  </id>

<name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id
type="integer">
  </parent-id>
  <position type="integer">

</position>
  <state></state>
  <text>Empty</text>
  <updated-at
type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by
type="integer">
  </updated-by>
  <uri></uri>
  <version
type="integer">
  </version>
</book>
"

Ugh, that’s ugly. Ruby’s print function might help us out here.

>> print Book.find(:first).to_xml
<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by type="integer"></created-by>
  <id type="integer">1</id>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id type="integer"></parent-id>
  <position type="integer"></position>
  <state></state>
  <text>Empty</text>
  <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by type="integer"></updated-by>
  <uri></uri>
  <version type="integer">
  </version>
</book>

Much better! So what do we have here? Looks like a fairly straightforward serialized representation of our Book instance in XML.

Customizing to_xml Output

The standard processing instruction is at the top, followed by a tag name corresponding to the class name of the object. The properties are represented as subelements, with nonstring data fields including a type attribute. Mind you, this is the default behavior and we can customize it with some additional parameters to the to_xml method.

We’ll strip down that XML representation of a book to just a name and URI using the only parameter. It’s provided in a familiar options hash, with the value of the :only parameter as an array:

>> print Book.find(:first).to_xml(:only => [:name,:uri])
<?xml version="1.0" encoding="UTF-8"?>
<book>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <uri></uri>
</book>

Following the familiar Rails convention, the only parameter is complemented by its inverse, except, which will exclude the specified properties.

What if I want my book title and URI as a snippet of XML that will be included in another document? Then let’s get rid of that pesky instruction too, using the skip_instruct parameter.

>> print Book.find(:first).to_xml(:skip_instruct => true, :only =>
[:name,:uri])
<book>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <uri></uri>
</book>

We can change the root element in our XML representation of Book and the indenting from two to four spaces by using the root and indent parameters respectively.

>> print Book.find(:first).to_xml(:root => 'textbook', :indent => 4)

<?xml version="1.0" encoding="UTF-8"?>
<textbook>
    <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
    <created-by type="integer"></created-by>
    <id type="integer">1</id>
    <name>Professional Ruby on Rails Developer's Guide</name>
    <parent-id type="integer"></parent-id>
    <position type="integer"></position>
    <state></state>
    <text>Empty</text>
    <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
    <updated-by type="integer"></updated-by>
    <uri></uri>
    <version type="integer">
    </version>
</textbook>

By default Rails converts CamelCase and underscore attribute names to dashes as in created-at and parent-id. You can force underscore attribute names by setting the dasherize parameter to false.

>> print Book.find(:first).to_xml(:dasherize => false, :only =>
[:created_at,:created_by])

<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created_at type="datetime">2007-07-02T13:58:19-05:00</created_at>
  <created_by type="integer"></created_by>
</book>

In the preceding output, the attribute type is included. This too can be configured using the skip_types parameter.

>> print Book.find(:first).to_xml(:skip_types => true, :only =>
[:created_at,:created_by])

<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at>2007-07-02T13:58:19-05:00</created-at>
  <created-by></created-by>
</book>

Associations and to_xml

So far we’ve only worked with a base ActiveRecord and not with any of its associations. What if we wanted an XML representation of not just a book but also its associated chapters? Rails provides the :include parameter for just this purpose. The :include parameter will also take an array or associations to represent in XML.

>> print Book.find(:first).to_xml(:include => :chapters)
<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by type="integer"></created-by>
  <id type="integer">1</id>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id type="integer"></parent-id>
  <position type="integer"></position>
  <state></state>
  <text>Empty</text>
  <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by type="integer"></updated-by>
  <uri></uri>
  <version type="integer">
  </version>
  <chapters>
    <chapter>
      <name>Introduction</name>
      <uri></uri>
    </chapter>
    <chapter>
      <name>Your Rails Decision</name>
      <uri></uri>
    </chapter>
  </chapters>
</book>

The to_xml method will also work on any array so long as each element in that array responds to to_xml. If we try to call to_xml on an array whose elements don’t respond to to_xml, we get this result:

>> [:cat,:dog,:ferret].to_xml
RuntimeError: Not all elements respond to to_xml
        from /activesupport/lib/active_support/core_ext/array/
        conversions.rb:48:in `to_xml'
        from (irb):6

Unlike arrays, Ruby hashes are naturally representable in XML, with keys corresponding to tag names, and their values corresponding to tag contents. Rails automatically calls to_s on the values to get string values for them.

>> print ({:pet => 'cat'}.to_xml)
<?xml version="1.0" encoding="UTF-8"?>
<hash>
  <pet>cat</pet>
</hash>

Both Array and Hash objects take the same to_xml method arguments, except :include.

Advanced to_xml

By default, ActiveRecord’s to_xml method only serializes persistent attributes into XML. However, there are times when transient, derived, or calculated values need to be serialized out into XML form as well. For example, our Book model could have a method that gives the average pages per chapter.

class Book < ActiveRecord::Base

  def pages_per_chapter
    self.pages / self.chapters.length
  end

end

To include the result of this method when we serialize the XML, we use the :methods parameter:

>> print Book.find(:first).to_xml(:methods => :pages_per_chapter)
<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by type="integer"></created-by>
  <id type="integer">1</id>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id type="integer"></parent-id>
  <position type="integer"></position>
  <state></state>
  <text>Empty</text>
  <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by type="integer"></updated-by>
  <uri></uri>
  <version type="integer"></version>
  <pages-per-chapter>45</pages-per-chapter>
</book>

We could also set the methods parameter to an array of method names to be called.

Dynamic Runtime Attributes

In cases where we want to include extra elements unrelated to the object being serialized, we can use the :procs option. Just pass one or more Proc instances. They will be called with to_xml’s option hash, through which we access the underlying XmlBuilder. (XmlBuilder provides the principal means of XML generation in Rails, and is covered later in this chapter in the section “The XML Builder.”)

>> copyright = Proc.new {|opts|
opts[:builder].tag!('copyright','2007')}

>> print Book.find(:first).to_xml(:procs => [copyright])

<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by type="integer"></created-by>
  <id type="integer">1</id>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id type="integer"></parent-id>
  <position type="integer"></position>
  <state></state>
  <text>Empty</text>
  <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by type="integer"></updated-by>
  <uri></uri>
  <version type="integer"></version>
  <color>blue</color >
</book>

Unfortunately, the :procs technique is hobbled by a puzzling limitation: The record being serialized is not exposed to the procs being passed in as arguments, so only data external to the object may be added in this fashion.

To gain complete control over the XML serialization of Rails objects, you need to override the to_xml method and implement it yourself.

Overriding to_xml

Sometimes you need to do something out of the ordinary when trying to represent data in XML form. In those situations you can create the XML by hand.

class Book < ActiveRecord::Base

  def to_xml(options = {})
   xml = options[:builder] ||= Builder::XmlMarkup.new(options)
   xml.instruct! unless options[:skip_instruct]
   xml.book do
     xml.tag!(:color, 'red')
   end
  end
  ...
end

This would give the following result:

>> print Book.find(:first).to_xml
<?xml version="1.0" encoding="UTF-8"?><book><color>red</color></book>

Learning from Array’s to_xml Method

Array’s to_xml method is a good example of the power and elegance possible when programming with Ruby. Let’s take a look at the code, which exists as part of Rails extensions to Ruby’s Array class, located in the ActiveSupport’s core_ext/array/conversions.rb.

def to_xml(options = {})
  raise "Not all elements respond to to_xml" unless all? { |e|
    e.respond_to? :to_xml }

See how close the first line is to English? The way that the to_xml method checks the elements of the array is a beautiful example of the readability achievable when programming in Ruby and the level of elegance you should be shooting for in your own code.

Moving on, we see how Rails figures out what to name the container tag.

options[:root]||= all? { |e|
  e.is_a?(first.class) && first.class.to_s != "Hash" } ?
  first.class.to_s.underscore.pluralize : "records"

First of all, the short-circuiting OR assignment ||= either uses the provided value for options[:root] or calculates it. This style of conditional assignment is a very common idiom in Ruby code and one you should get accustomed to using. If options[:root] is nil, a bit of logic takes place, starting with a check to see if all of the elements are instances of the same class (and that those instances are not hashes).

If that condition is true, that is, if all the elements are of the same type as the first element of the array, then the following expression generates our container tag name: first.class.to_s.underscore.pluralize.

Otherwise, the container tag will default to the constant "records", a fact that is not mentioned in the Rails API documentation. When I was looking through this code, I asked myself, “What does that first variable refer to?”

Then I remembered that this code executes in the context of an Array instance, so first is actually a method call that returns the first element of the array.

Cool. Let’s move ahead to the next line of the to_xml method, which governs the name used for the tags of the array’s elements: options[:children] ||= options[:root].singularize.

That was easy. Unless it’s configured explicitly, Rails will simply use the singular inflection of the container tag. One of the first things we learn in the Rails world is how ActiveRecord automatically figures out plural and singular forms in relation to class names and database tables. What many of us don’t usually realize until much later is the importance of the Inflector class and how widely it is used in the rest of the Rails codebase. Hopefully this walk-through is reinforcing the importance of cooperating with the Rails Inflector instead of working against it by configuring names manually.

What about the indentation? It defaults to two spaces: options[:indent] ||= 2.

Now things start getting a little more interesting. As we can see in the next line, the to_xml method uses Builder::XmlMarkup to do its XML generation.

options[:builder]  ||=
  Builder::XmlMarkup.new(:indent => options[:indent])

The :builder option allows us to pass in an existing Builder instance instead of using a new one, and the importance of this option will become clearer later on in the chapter when we discuss how to integrate the use of the to_xml method into more specialized XML generation routines.

root = options.delete(:root).to_s
children = options.delete(:children)

We’re going to need those values for root and children tag names, so we capture them at the same time that we remove them from the options hash. This is our first hint that the options hash is going to get reused for another call (when it comes time to generate XML for our child elements).

if !options.has_key?(:dasherize) || options[:dasherize]
  root = root.dasherize
end

The :dasherize option defaults to true, which makes sense since conventions in the XML world dictate that compound tag names are delimited by dashes. It’s hard to overemphasize how much of Rails’ code elegance comes from the way that its libraries build on each other, as demonstrated by this use of the whimsically named dasherize.

Moving on, we come to our :instruct parameter, discussed earlier in the chapter. Builder has an instruct! method, which causes the XML instruction line to be inserted. Of course, once it’s inserted, we don’t want to insert it again, which is why the options hash that we will use recursively now gets its :skip_instruct parameter hard-coded to true.

options[:builder].instruct! unless options.delete(:skip_instruct)
opts = options.merge({:skip_instruct => true, :root => children })

Finally, we invoke tag! on our XML builder to actually write the container XML, followed immediately by a recursive call (via the each method) that calls to_xml on our child elements.

  options[:builder].tag!(root) { each { |e| e.to_xml(opts) } }
end

The XML Builder

As introduced in the previous section, Builder::XmlMarkup is the class used internally by Rails when it needs to generate XML. When to_xml is not enough and you need to generate custom XML, you will use Builder instances directly. Fortunately, the Builder API is one of the most powerful Ruby libraries available and is very easy to use, once you get the hang of it.

The API documentation says: “All (well, almost all) methods sent to an XmlMarkup object will be translated to the equivalent XML markup. Any method with a block will be treated as an XML markup tag with nested markup in the block.”

That is actually a very concise way of describing how Builder works, but it is easier to understand with some examples, again taken from Builder’s API documentation. The xm variable is a Builder::XmlMarkup instance:

xm.em("emphasized")            # => <em>emphasized</em>
xm.em { xm.b("emp & bold") }   # => <em><b>emph &amp; bold</b></em>

xm.a("foo", "href"=>"http://foo.org")
                               # => <a href="http://foo.org">foo</a>

xm.div { br }                  # => <div><br/></div>

xm.target("name"=>"foo", "option"=>"bar")
                               # => <target option="foo" name="bar">

xm.instruct!                   # <?xml version="1.0" encoding="UTF-8"?>

xm.html {                      # <html>
  xm.head {                    #   <head>
    xm.title("History")        #     <title>History</title>
  }                            #   </head>
  xm.body {                    #   <body>
    xm.comment! "HI"           #     <!-- HI -->
    xm.h1("Header")            #     <h1>Header</h1>
    xm.p("paragraph")          #     <p>paragraph</p>
  }                            #   </body>
}                              # </html>

A common use for using Builder::XmlBuilder is to render XML in response to a request. Previously we talked about overriding to_xml on ActiveRecord to generate our custom XML. Another way, though not as recommended, is to use an XML template.

We could alter our BooksController show method to use an XML template by changing it from

def BooksController < ApplicationController
  ...
  def show
    @book = Book.find(params[:id])
    respond_to do |format|
    format.html
    format.xml { render :xml => @book.to_xml }
  end
  ...
end

to:

def BooksController < ApplicationController
  ...
  def show
    @book = Book.find(params[:id])
    respond_to do |format|
    format.html
    format.xml
  end
  ...
end

Now Rails will look for a file called show.xml.builder in the RAILS_ROOT/views/books directory. That file contains Builder::XmlMarkup code like this:

xml.book {
  xml.title @book.title
  xml.chapters {
    @book.chapters.each { |chapter|
      xml.chapter {
        xml.title chapter.title
      }
    }
  }
}

In this view the variable xml is an instance of Builder::XmlMarkup. Just as in ERb views, we have access to the instance variables we set in our controller, in this case @book. Using the Builder in a view can provide a convenient way to generate XML.

Parsing XML

Ruby has a full-featured XML library named REXML, and covering it in any level of detail is outside the scope of this book. If you have basic parsing needs, such as parsing responses from web services, you can use the simple XML parsing capability built into Rails.

Turning XML into Hashes

Rails lets you turn arbitrary snippets of XML markup into Ruby hashes, with the from_xml method that it adds to the Hash class.

To demonstrate, I’ll throw together a string of simplistic XML and turn it into a hash:

>> xml = <<-XML
<pets>
  <cat>Franzi</cat>
  <dog>Susie</dog>
  <horse>Red</horse>
</pets>
XML

>> Hash.from_xml(xml)
=> {"pets"=>{"horse"=>"Red", "cat"=>"Franzi", "dog"=>"Susie"}}

There are no options for from_xml. You can leave off the argument, pass it a string of XML, or pass it an IO object. If you pass nothing, the from_xml method looks for a file named scriptname.xml (or more correctly $0.xml). This isn’t immediately useful in Rails, but can be handy if you use this functionality in your own scripts outside of Rails HTTP request handling.

A more common use is to pass a string into from_xml as in the preceding example or to pass it an IO object. This is particularly useful when parsing an XML file.

>> Hash.from_xml(File.new('pets.xml')
=> {"pets"=>{"horse"=>"Red", "cat"=>"Franzi", "dog"=>"Susie"}}

XmlSimple

Under the covers, Rails uses a library called XmlSimple to parse XML into a Hash.

class Hash

  ...

  def from_xml(xml)
    typecast_xml_value(undasherize_keys(XmlSimple.xml_in(xml,
              'keeproot'     => true,
              'forcearray'   => false,
              'forcecontent' => true,
              'contentkey'   => '__content__')
            ))
  end
  ...
end

Rails sets four parameters when using XmlSimple. The first parameter, :keeproot, tells XmlSimple not to discard the root element, which it would otherwise do by default.

>> XmlSimple.xml_in('<book title="The Rails Way" />', :keeproot =>
true)
=> { 'book' => [{'title' => 'The Rails Way'}]

>> XmlSimple.xml_in('<book title="The Rails Way" />', :keeproot =>
false)
=> {'title' => 'The Rails Way'}

The second parameter Rails sets is :forcearray, which forces nested elements to be represented as arrays even if there is only one. XmlSimple’s default is to set this to true. The difference is shown in the following example:

>> XmlSimple.xml_in('<book><chapter index="1"/></book>', :forcearray =>
true)
=> {"chapter"=>[{"index"=>"1"}]}

>> XmlSimple.xml_in('<book><chapter index="1"/></book>', :forcearray =>
false)
=> {"chapter" => {"index"=> "1"}}

The third parameter that’s set to true is :forcecontent, which ensures that a content key-value pair is added to the resulting hash even if the element being parsed has no content or attributes. By setting this parameter to true, sibling elements are normalized, which makes the resulting hash a heck of a lot more usable, as you should be able to deduce from the following snippet.

>> XmlSimple.xml_in('<book>
                       <chapter index="1">Words</chapter>
                       <chapter>Numbers</chapter>
                     </book>', :forcecontent => true)

=> {"chapter" => [{"content"=>"Words", "index"=>"1"},
{"content"=>"Numbers"}]}

>> XmlSimple.xml_in('<book>
                       <chapter index="1">Words</chapter>
                       <chapter>Numbers</chapter>
                     </book>', :forcecontent => false)

=> {"chapter" => [{"content"=>"Words", "index"=>"1"}, "Numbers"]}

The final parameter is :contentkey. XmlSimple by default uses the key string '"content" to represent the data contained within an element. Rails changes it to "__content__" to lessen the likelihood of name clashes with actual XML tags named "content".

Typecasting

When we use Hash.from_xml, the resulting hash doesn’t have any "__content__" keys. What happened to them? Rails doesn’t pass the result of XmlSimple parsing directly back to the caller of from_xml. Instead it sends it through a method called typecast_xml_value, which converts the string values into proper types. This is done by using a type attribute in the XML elements. For example, here’s the autogenerated XML for a Book object.

>> print Book.find(:first).to_xml
<?xml version="1.0" encoding="UTF-8"?>
<book>
  <created-at type="datetime">2007-07-02T13:58:19-05:00</created-at>
  <created-by type="integer"></created-by>
  <id type="integer">1</id>
  <name>Professional Ruby on Rails Developer's Guide</name>
  <parent-id type="integer"></parent-id>
  <position type="integer"></position>
  <state></state>
  <text>Empty</text>
  <updated-at type="datetime">2007-07-02T13:58:19-05:00</updated-at>
  <updated-by type="integer"></updated-by>
  <uri></uri>
  <version type="integer">
  </version>
</book>

As part of the to_xml method, Rails sets attributes called type that identify the class of the value being serialized. If we take this XML and feed it to the from_xml method, Rails will typecast the strings to their corresponding Ruby objects:

>> Hash.from_xml(Book.find(:first).to_xml)
=> {"book"=>{"name"=>"Professional Ruby on Rails Developer's Guide",
"uri"=>nil, "updated_at"=>Mon Jul 02 18:58:19 UTC 2007,
"text"=>"Empty", "created_by"=>nil, "id"=>1, "updated_by"=>nil,
 "version"=>0, "parent_id"=>nil, "position"=>nil, "created_at"=>Mon Jul
02 18:58:19 UTC 2007, "state"=>nil}}

ActiveResource

Web applications often need to serve both users in front of web browsers and other systems via some API. Other languages accomplish this using SOAP or some form of XML-RPC, but Rails takes a simpler approach. In Chapter 4, “REST, Resources, and Rails,” we talked about building RESTful controllers and using respond_to to return different representations of resources. By doing so we could connect to http://localhost:3000/auctions.xml and get back an XML representation of all auctions in the system. We can now write a client to consume this data using ActiveResource.

ActiveResource is a standard part of the Rails package, having replaced ActionWebService (which is still available as a plugin). ActiveResource has complete understanding of RESTful routing and XML representation. A minimal ActiveResource for the previous auctions example is

class Auction < ActiveResource::Base
  self.site = 'http://localhost:3000'
end

To get a list of auctions we would call its find method:

>> auctions = Auction.find(:all)

ActiveResource is designed to look and feel much like ActiveRecord.

Find

ActiveResource has the same find methods as ActiveRecord, as seen in Table 15.1. The only difference is the use of :params instead of :conditions.

Table 15.1. Find methods for ActiveResource

ActiveRecord

ActiveResource

URL

Auction.find(:all)

Auction.find(:all)

GET http://localhost:3000/auctions.xml

Auction.find(1)

Auction.find(1)

GET http://localhost:3000/auctions/1.xml

Auction.find(:first)

Auction.find(:first)

GET http://localhost:3000/auctions.xml *gets a complete list than calls first on the returned list

Auction.find(:all, :conditions => { :first_name => 'Matt')

Auction.find(:all, :params => { :first_name => 'Matt')

GET http://localhost:3000/auctions.xml?first_name=Matt

Item.find(:all, :conditions => { :auction_id => 6 })

Item.find(:all, :params => { :auction_id => 6 })

GET http://localhost:3000/auctions/6/items.xml

Item.find(:all, :conditions => { :auction_id => 6, :used => true })

Item.find(:all, :params => { :auction_id => 6, :used => true })

GET http://localhost:3000/auctions/6/items.xml?used=true

The last two examples in Table 15.1 show how to use ActiveResource with a nested resource. We could also create a custom used method in our items controller like this:

class ItemController < ActiveResource::Base

  def used
    @items = Item.find(:all,
               :conditions => {:auction_id => params[:auction_id],
                               :used => true })

    respond_to do |format|
      format.html
      format.xml { render :xml => @items.to_xml }
    end
  end
end

In our routes.rb file we would add to our items resource like this:

map.resources :items, :member => {:used => :get }

With this in place we now have the following URL:

http://localhost:3000/auctions/6/items/used.xml

We can now access this URL and the data behind it using ActiveResource with the following call:

>> used_items = Item.find(:all, :from => :used)

This custom method returns a collection of items and hence the :all parameter. Suppose we had a custom method that returned only the newest item, as in the following example:

class ItemController < ActiveResource::Base

  def newest
    @item = Item.find(:first,
              :conditions => {:auction_id => params[:auction_id]},
              :order => 'created_at DESC',
              :limit => 1)

    respond_to do |format|
      format.html
      format.xml { render :xml => @items.to_xml }
    end
  end
end

We could then make the following call:

>> used_items = Item.find(:one, :from => :newest)

What’s important to note is how a request to a nonexistent item is handled. If we tried to access an item with an id of -1 (there isn’t any such item), we would get an HTTP 404 status code back. This is exactly what ActiveResource receives and raises a ResourceNotFound exception. ActiveResource makes heavy use of the HTTP status codes as we’ll see throughout this chapter.

Create

ActiveResource is not limited to just retrieving data; it can also create it. If we wanted to place a new bid on an item via ActiveResource, we would do the following:

>> Bid.create(:username => 'me', :auction_id => 3, :item_id => 6,
:amount => 34.50)

This would create an HTTP POST to the URL: http://localhost:3000/auctions/6/items/6.xml with the supplied data. In our controller, the following would exist:

class BidController < ActiveResource::Base
  ...
  def create
    @bid = Bid.new(params[:bid])
    respond_to do |format|
      if @bid.save
       flash[:notice] = 'Bid was successfully created.'
       format.html { redirect_to(@bid) }
       format.xml  { render :xml => @bid, :status => :created,
:location => @bid }
      else
       format.html { render :action => "new" }
       format.xml  { render :xml => @bid.errors, :status =>
:unprocessable_entity}
      end
    end
  end
  ...
end

If the bid is successfully created, the newly created bid is returned with an HTTP 201 status code and the Location header is set pointing to the location of the newly created bid. With the Location header set, we can determine what the newly created bid’s id is. For example:

>> bid = Bid.create(:username => 'me', :auction_id => 3, :item_id =>
6, :amount => 34.50)
>> bid.id # => 12
>> bid.new? # => false

If we tried to create the preceding bid again but without a dollar amount, we could interrogate the errors.

>> bid = Bid.create(:username => 'me', :auction_id => 3, :item_id => 6)
>> bid.valid? # => false
>> bid.id # => nil
>> bid.new? # => true
>> bid.errors.class # => ActiveResource::Errors
>> bid.errors.size # => 1
>> bid.errors.on_base # => "Amount can't be blank"
>> bid.errors.full_messages # => "Amount can't be blank"
>> bid.errors.on(:amount) # => nil

In this case a new Bid object is returned from the create method, but it’s not valid. If we try to see what its id is we also get a nil. We can see what caused the create to fail by calling the ActiveResources.errors method. This method behaves just like ActiveRecord.error with one important exception. On ActiveRecord if we called Errors.on, we would get the error for that attribute. In the preceding example, we got a nil instead. The reason is that ActiveResource, unlike ActiveRecord, doesn’t know what attributes there are. ActiveRecord does a SHOW FIELDS FROM <table> to get this, but ActiveResource has no equivalent. The only way ActiveResource knows an attribute exists is if we tell it. For example:

>> bid = Bid.create(:username => 'me', :auction_id => 3, :item_id =>
6, :amount => nil)
>> bid.valid? # => false
>> bid.id # => nil
>> bid.new? # => true
>> bid.errors.class # => ActiveResource::Errors
>> bid.errors.size # => 1
>> bid.errors.on_base # => "Amount can't be blank"
>> bid.errors.full_messages # => "Amount can't be blank"
>> bid.errors.on(:amount) # => "can't be blank"

In this case we told ActiveResource that there is a title attribute through the create method. As a result we can now call Errors.on without a problem.

Update

Editing an ActiveResource follows the same ActiveRecord pattern.

>> bid = Bid.find(1)
>> bid.amount # => 10.50
>> bid.amount = 15.00
>> bid.save # => true
>> bid.reload
>> bid.amount # => 15.00

If we set the amount to nil, ActiveResource.save would return false. In this case we could interrogate ActiveResource::Errors for the reason, just as we would with create. An important difference between ActiveResource and ActiveRecord is the absence of the save! and update! methods.

Delete

Removing an ActiveResource can happen in two ways. The first is without instantiating the ActiveResource:

>> Bid.delete(1)

The other way requires instantiating the ActiveResource first:

>> bid = Bid.find(1)
>> bid.destroyAuthorization

ActiveResource comes with support for HTTP Basic Authentication. As a quick reminder, Basic Authentication is accomplished by setting an HTTP header, and as such can be easily snooped. For this reason, an HTTPS connection should be used. With a secure connection in place, ActiveResource just needs a username and password to connect.

Class MoneyTransfer < ActiveResource::Base
  self.site = 'https://localhost:3000'
  self.username = 'administrator'
  self.password = 'secret'
end

ActiveResource will now authenticate on each connection. If the username and/or password is invalid, an ActiveResource::ClientError is generated. We can implement Basic Authentication in our controller using a plugin.

$ ./script/plugin install http_authentication

Next we need to set up our controller:

class MoneyTransferController < ApplicationController
  USERNAME, PASSWORD = "administrator", "secret"

  before_filter :authenticate

  ...
  def create
    @money_transfer = Bid.new(params[:money_transfer])
    respond_to do |format|
      if @ money_transfer.save
       flash[:notice] = 'Money Transfer was successfully created.'
       format.html { redirect_to(@money_transfer) }
       format.xml  { render :xml => @ money_transfer, :status =>
:created, :location => @ money_transfer }
      else
       format.html { render :action => "new" }
       format.xml  { render :xml => @ money_transfer.errors, :status
=> :unprocessable_entity}
      end
    end
  end
  ...

  private
    def authenticate
      authenticate_or_request_with_http_basic do |username, password|
        username == USERNAME && password == PASSWORD
      end
    end
end

Headers

ActiveResource allows for the setting of HTTP headers on each request too. This can be done in two ways. The first is to set it as a variable:

Class Auctions< ActiveResource::Base
  self.site = 'http://localhost:3000'

  @headers = { 'x-flavor' => 'orange' }
end

This will cause every connection to the site to include the HTTP header: HTTP-X-FLAVOR: orange. In our controller we could use the header value.

class AuctionController < ActiveResource::Base
  ...
  def show
      @auction = Auction.find_by_id_and_flavor(params[:bid],
request.headers['HTTP_X_FLAVOR'])    respond_to do |format|
      format.html
      format.xml { render :xml => @auction.to_xml }
    end
  end
  ...
end

The second way to set the headers for an ActiveResource is to override the headers method.

Class Auctions< ActiveResource::Base
  self.site = 'http://localhost:3000'

  def headers
    { 'x-flavor' => 'orange' }
  end
end

Customizing

ActiveResource assumes RESTful URLs, but that doesn’t always happen. Fortunately, you can customize the URL prefix and collection_name. Suppose we assume the following ActiveResource:

Class OldAuctionSystem < ActiveResource::Base
  self.site = 'http://s60:3270'

  self.prefix = '/cics/'
  self.collection_name = 'auction_pool'
end

The following URLs will be used:

OldAuctionSystem.find(:all) GET http://s60:3270/cics/auction_pool.xml

OldAuctionSystem.find(1) GET http://s60:3270/cics/auction_pool/1.xml

OldAuctionSystem.find(1).save PUT http://s60:3270/cics/auction_pool/1.xml

OldAuctionSystem.delete(1) DELETE http://s60:3270/cics/auction_pool/1.xml

OldAuctionSystem.create(...) POST http://s60:3270/cics/auction_pool.xml

We could also change the element name used to generate XML. In the preceding ActiveResource, a create of an OldAuctionSystem would look like the following in XML:

<?xml version="1.0" encoding="UTF-8"?>
  <OldAuctionSystem>
    <title>Auction A</title>
    ...
  </OldAuctionSystem>

The element name can be changed with the following:

Class OldAuctionSystem < ActiveResource::Base
  self.site = 'http://s60:3270'

  self.prefix = '/cics/'
  self.element_name = 'auction'
end

which will produce:

<?xml version="1.0" encoding="UTF-8"?>
<Auction>
  <title>Auction A</title>
  ...
</Auction>

One consequence of setting the element_name is that ActiveResource will use the plural form to generate URLs. In this case it would be 'auctions' and not 'OldAuctionSystems'. To do this you will need to set the collection_name as well.

It is also possible to set the primary key field ActiveResource uses with

Class OldAuctionSystem < ActiveResource::Base
  self.site = 'http://s60:3270'

  self.primary_key = 'guid'
end

Hash Forms

The methods Find, Create, Save, and Delete correspond to the HTTP methods of GET, POST, PUT, and DELETE respectively. ActiveResource has a method for each of these HTTP methods too. They take the same arguments as Find, Create, Save, and Delete but return a hash of the XML received. For example:

>> bid = Bid.find(1)
>> bid.class # => ActiveRecord::Base
>> bid_hash = Bid.get(1)
>> bid_hash.class # => Hash

Conclusion

In practice, the to_xml and from_xml methods meet the XML handling needs for most situations that the average Rails developer will ever encounter. Their simplicity masks a great degree of flexibility and power, and in this chapter we attempted to explain them in sufficient detail to inspire your own exploration of XML handling in the Ruby world.

As a pair, the to_xml and from_xml methods also enabled the creation of a framework that makes tying Rails applications together using RESTful web services drop-dead easy. That framework is named ActiveResource, and this chapter gave you a crash-course introduction to it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.20