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.
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.
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>
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
.
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.
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.
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>
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
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 & 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.
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.
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"}}
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"
.
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}}
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
.
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
|
| URL |
---|---|---|
|
| GET http://localhost:3000/auctions.xml |
|
| GET http://localhost:3000/auctions/1.xml |
|
| GET http://localhost:3000/auctions.xml *gets a complete list than calls first on the returned list |
|
| GET http://localhost:3000/auctions.xml?first_name=Matt |
|
| GET http://localhost:3000/auctions/6/items.xml |
|
| 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.
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.
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.
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
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
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
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
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.
3.22.216.254