Often you'll find yourself reading an XML file and writing boilerplate code with REXML.
I was in the middle of a tool that'd generate Ruby code from an XML Schema when I started feeling the pain of working directly with REXML. From that point it was only a matter of hours to come up with this simple and elegant solution:
require 'rexml/document' class XMLDef attr_accessor :text def self.xml_attribute(attr_name, xml_elem) attr_accessor attr_name @attributes = Hash.new unless defined?(@attributes) @attributes[attr_name] = xml_elem end def self.xml_array(array_name, xml_elem, xml_class) attr_reader array_name @arrays = Hash.new unless defined?(@arrays) @arrays[array_name] = [xml_elem, xml_class] end def self.xml_object(object_ref_name, xml_elem, xml_class) attr_reader object_ref_name @objects = Hash.new unless defined?(@objects) @objects[object_ref_name] = [xml_elem, xml_class] end def self.load_element(elem) result = new result.text = elem.text if @attributes @attributes.each do |k, v| result.send(k.to_s + '=', elem.attributes[v]) end end if @arrays @arrays.each do |k, v| result.instance_variable_set('@' + k.to_s, Array.new) elem.each_element(v[0]) do |x| result.send(k).send('<<', v[1].load_element(x)) end end end if @objects @objects.each do |k, v| if x = elem.elements[v[0]] result.instance_variable_set('@' + k.to_s, v[1].load_element(x)) end end end return result end def self.load_xml(xml) return load_element(REXML::Document.new(xml).root) end end
To create a mapping you simply inherit from XMLDef. xml_attribute, xml_array and xml_object create the accessors. load_element recursively reads the XML into the objects you define. load_xml is the method you call on the root of your mapping to read the actual XML.
For example, suppose you want a mapping for the following XML:
<planetSystem name='Solar System'>
<dimensions diameter='100000 AU' mass='2e30 kg'/>
<planet name='Earth' lifeFriendly='1'>
<dimensions diameter='12745 km' mass='5.97e24 kg'/>
<description>The planet were we live</description>
</planet>
<planet name='Mars' lifeFriendly='0'>
<dimensions diameter='6805 km' mass='6.42e23 kg'/>
</planet>
<planet name='Jupiter'>
<description>Biggest planet in the solar system</description>
</planet>
</planetSystem>
You'll write:
class Description < XMLDef end class Dimensions < XMLDef xml_attribute :diameter, "diameter" xml_attribute :mass, "mass" end class Planet < XMLDef xml_attribute :name, "name" xml_attribute :life_friendly, "lifeFriendly" xml_object :dimensions, "dimensions", Dimensions xml_object :description, "description", Description end class PlanetSystem < XMLDef xml_attribute :name, "name" xml_array :planets, "planet", Planet xml_object :dimensions, "dimensions", Dimensions end
I don't think you could write anything more obvious. The annotations make the code pretty straightforward to understand and maintain. I was coding a lot of test-cases for my XML Schema reader. The mapper rendered them completely useless.
This is how you'd read the XML above:
x = PlanetSystem.load_xml(File.open('planets.xml')) puts x.name puts "Diameter: " + x.dimensions.diameter puts "Mass: " + x.dimensions.mass puts "" p = x.planets[0] puts p.name + ': ' + p.description.text puts "Life friendly: " + p.life_friendly puts "Diameter: " + p.dimensions.diameter puts "Mass: " + p.dimensions.mass
That's not hard to extend the class to save data back to XML or create a new one from scratch.
Today, after spending a few hours coding the class above, I found ROXML, which follows the same idea as mine. Some of the similarities are interesting, by the way. ROXML also has the methods xml_attribute and xml_object. And an example included in the source code references the Pickaxe book, which I did in the test-cases for XMLDef. I guess the author also had the book over the table when thinking out an example.
0 comments:
Post a Comment