In which I show that XML does not have to suck—instead you can just HappyMap it!
As much as I write about XML, you would swear it is all I do, but I promise it is not. In fact, I do not really use XML that often, but I will admit that I am intrigued by it. A while back, you may remember, I posted about ROXML, a ruby object to xml mapping library. I liked the idea but not the implementation. Soon after, I started playing around with what I have named HappyMapper, a ruby object to xml mapping library.
I wrote nearly 95% of it in a weekend and then let it sit. I let it sit so long that it started to rot. Today it hit me that I do not have to finish something in order to release it. The thing that wasn’t working was xml with a default namespace. For good reasons I am sure, libxml-ruby does not like having default namespaces. I thought to myself, you know, this library is cool even without namespace junk. I mean who even uses namespaces other than Amazon. I started to package it for release and then I noticed a few nitpicky things. I tweaked them and five hours later I had also fixed the namespace issue and changed the API a bit. So much for releasing unfinished code in hopes that someone smarter than I would finish it up…
Examples
But I digress, you do not care about all that, right? How about some examples? Twitter’s xml seems to be popular on this here blawg, so I will start with that. Given this xml sample from twitter:
<statuses type="array">
<status>
<created_at>Sat Aug 09 05:38:12 +0000 2008</created_at>
<id>882281424</id>
<text>I so just thought the guy lighting the Olympic torch was falling when he began to run on the wall. Wow that would have been catastrophic.</text>
<source>web</source>
<truncated>false</truncated>
<in_reply_to_status_id>1234</in_reply_to_status_id>
<in_reply_to_user_id>12345</in_reply_to_user_id>
<favorited></favorited>
<user>
<id>4243</id>
<name>John Nunemaker</name>
<screen_name>jnunemaker</screen_name>
<location>Mishawaka, IN, US</location>
<description>Loves his wife, ruby, notre dame football and iu basketball</description>
<profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/53781608/Photo_75_normal.jpg</profile_image_url>
<url>http://addictedtonew.com</url>
<protected>false</protected>
<followers_count>486</followers_count>
</user>
</status>
</statuses>
You could setup the following ruby objects:
class User
include HappyMapper
element :id, Integer
element :name, String
element :screen_name, String
element :location, String
element :description, String
element :profile_image_url, String
element :url, String
element :protected, Boolean
element :followers_count, Integer
end
class Status
include HappyMapper
element :id, Integer
element :text, String
element :created_at, Time
element :source, String
element :truncated, Boolean
element :in_reply_to_status_id, Integer
element :in_reply_to_user_id, Integer
element :favorited, Boolean
has_one :user, User
end
statuses = Status.parse(xml_string)
statuses.each do |status|
puts status.user.name, status.user.screen_name, status.text, status.source, ''
end
You can note a few things about HappyMapper from that example.
- Each xml element and attribute can be typecast.
- You can define association-like elements that are formed from other HappyMapper objects (see
has_one :user, User in Status).
- You get a parse method when including HappyMapper that takes a string and does all the magic for you.
That was an easy one, how about something more complex and ugly, like some Amazon xml. Given some Amazon xml such as this:
<?xml version="1.0" encoding="UTF-8"?>
<ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05">
<OperationRequest>
<HTTPHeaders>
<Header Name="UserAgent">
</Header>
</HTTPHeaders>
<RequestId>16WRJBVEM155Q026KCV1</RequestId>
<Arguments>
<Argument Name="SearchIndex" Value="Books"></Argument>
<Argument Name="Service" Value="AWSECommerceService"></Argument>
<Argument Name="Title" Value="Ruby on Rails"></Argument>
<Argument Name="Operation" Value="ItemSearch"></Argument>
<Argument Name="AWSAccessKeyId" Value="dontbeaswoosh"></Argument>
</Arguments>
<RequestProcessingTime>0.064924955368042</RequestProcessingTime>
</OperationRequest>
<Items>
<Request>
<IsValid>True</IsValid>
<ItemSearchRequest>
<SearchIndex>Books</SearchIndex>
<Title>Ruby on Rails</Title>
</ItemSearchRequest>
</Request>
<TotalResults>22</TotalResults>
<TotalPages>3</TotalPages>
<Item>
<ASIN>0321480791</ASIN>
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh</DetailPageURL>
<ItemAttributes>
<Author>Michael Hartl</Author>
<Author>Aurelius Prochazka</Author>
<Manufacturer>Addison-Wesley Professional</Manufacturer>
<ProductGroup>Book</ProductGroup>
<Title>RailsSpace: Building a Social Networking Website with Ruby on Rails (Addison-Wesley Professional Ruby Series)</Title>
</ItemAttributes>
</Item>
</Items>
</ItemSearchResponse>
You could create the following objects to obtain Item information:
module PITA
class Item
include HappyMapper
tag 'Item' # if you put class in module you need tag
element :asin, String, :tag => 'ASIN'
element :detail_page_url, String, :tag => 'DetailPageURL'
element :manufacturer, String, :tag => 'Manufacturer', :deep => true
end
class Items
include HappyMapper
tag 'Items' # if you put class in module you need tag
element :total_results, Integer, :tag => 'TotalResults'
element :total_pages, Integer, :tag => 'TotalPages'
has_many :items, Item
end
end
item = PITA::Items.parse(xml_string, :single => true, :use_default_namespace => true)
item.items.each do |i|
puts i.asin, i.detail_page_url, i.manufacturer, ''
end
The previous example showed a few more things.
- You can put your HappyMapper objects in a module and define the tag name (see tag ‘Item’ inside PITA::Item).
- You can create nice methods for crappy camel cased xml tags (see
element :total_pages, Integer, :tag => 'TotalPages' in PITA::Items).
- There is also a has_many association-like method that allows defining a collection of HappyMapper objects. (see
has_many :items, Item in PITA::Items)
- You do not have to map exact parent child relationships. You can go deep see diving with the :deep option on any element to pluck out grandchildren and such. (see
element :manufacturer in PITA::Item)
Installation
Installation is typical as the gem is on rubyforge and github.
#rubyforge
$ sudo gem install happymapper
# github
$ sudo gem install jnunemaker-happymapper
If you run into problems, feel free to fork and add some specs for the xml it is not working with. From there you can dive in and fix them or let me know and I will take a look. Think this will be handy? Got an idea? Let me know in the comments below. Oh, and yes, in the future, HappyMapper will have killer HTTParty integration.
