module XML::Mapping
This is the central interface module of the xml-mapping library.
Including this module in your classes adds XML mapping capabilities to them.
Example¶ ↑
Input document:¶ ↑
<company name="ACME inc."> <address> <city>Berlin</city> <zip>10113</zip> </address> <customers> <customer id="jim"> <name>James Kirk</name> </customer> <customer id="ernie"> <name>Ernie</name> </customer> <customer id="bert"> <name>Bert</name> </customer> </customers> </company>
mapping class declaration:¶ ↑
require 'xml/mapping' ## forward declarations class Address; end class Customer; end class Company include XML::Mapping text_node :name, "@name" object_node :address, "address", :class=>Address array_node :customers, "customers", "customer", :class=>Customer end class Address include XML::Mapping text_node :city, "city" numeric_node :zip, "zip" end class Customer include XML::Mapping text_node :id, "@id" text_node :name, "name" def initialize(id,name) @id,@name = [id,name] end end
usage:¶ ↑
c = Company.load_from_file('company.xml') => #<Company:0x007ff64a1077a8 @name="ACME inc.", @address=#<Address:0x007ff64a106f60 @city="Berlin", @zip=10113>, @customers=[#<Customer:0x007ff64a105700 @id="jim", @name="James Kirk">, #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie">, #<Customer:0x007ff64a0ff030 @id="bert", @name="Bert">]> c.name => "ACME inc." c.customers.size => 3 c.customers[1] => #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie"> c.customers[1].name => "Ernie" c.customers[0].name => "James Kirk" c.customers[0].name = 'James Tiberius Kirk' => "James Tiberius Kirk" c.customers << Customer.new('cm','Cookie Monster') => [#<Customer:0x007ff64a105700 @id="jim", @name="James Tiberius Kirk">, #<Customer:0x007ff64a1045a8 @id="ernie", @name="Ernie">, #<Customer:0x007ff64a0ff030 @id="bert", @name="Bert">, #<Customer:0x007ff64a0f78f8 @id="cm", @name="Cookie Monster">] xml2 = c.save_to_xml => <company name='ACME inc.'> ... </> xml2.write($stdout,2) <company name='ACME inc.'> <address> <city> Berlin </city> <zip> 10113 </zip> </address> <customers> <customer id='jim'> <name> James Tiberius Kirk </name> </customer> <customer id='ernie'> <name> Ernie </name> </customer> <customer id='bert'> <name> Bert </name> </customer> <customer id='cm'> <name> Cookie Monster </name> </customer> </customers> </company>#
So you have to include XML::Mapping into your class to turn it into a “mapping class”, that is, to add XML mapping capabilities to it. An instance of the mapping classes is then bidirectionally mapped to an XML node (i.e. an element), where the state (simple attributes, sub-objects, arrays, hashes etc.) of that instance is mapped to sub-nodes of that node. In addition to the class and instance methods defined in XML::Mapping, your mapping class will get class methods like 'text_node', 'array_node' and so on; I call them “node factory methods”. More precisely, there is one node factory method for each registered node type. Node types are classes derived from XML::Mapping::Node; they're registered with the xml-mapping library via ::add_node_class. The node types TextNode, BooleanNode, NumericNode, ObjectNode, ArrayNode, and HashNode are automatically registered by xml/mapping.rb; you can easily write your own ones. The name of a node factory method is inferred by 'underscoring' the name of the corresponding node type; e.g. 'TextNode' becomes 'text_node'. Each node factory method creates an instance of the corresponding node type and adds it to the mapping class (not its instances). The arguments to a node factory method are automatically turned into arguments to the corresponding node type's initializer. So, in order to learn more about the meaning of a node factory method's parameters, you read the documentation of the corresponding node type. All predefined node types expect as their first argument a symbol that names an r/w attribute which will be added to the mapping class. The mapping class is a normal Ruby class; you can add constructors, methods and attributes to it, derive from it, derive it from another class, include additional modules etc.
Including XML::Mapping also adds all methods of XML::Mapping::ClassMethods to your class (as class methods).
It is recommended that if your class does not have required
initialize
method arguments. The XML
loader attempts to create a new object using the new
method.
If this fails because the initializer expects an argument, then the loader
calls allocate
instead. allocate
bypasses the
initializer. If your class must have initializer arguments, then you
should verify that bypassing the initializer is acceptable.
As you may have noticed from the example, the node factory methods generally use XPath expressions to specify locations in the mapped XML document. To make this work, XML::Mapping relies on XML::XXPath, which implements a subset of XPath, but also provides write access, which is needed by the node types to support writing data back to XML. Both XML::Mapping and XML::XXPath use REXML (www.germane-software.com/software/rexml/) to represent XML elements/documents in memory.
Constants
- VERSION
Public Class Methods
Registers the new node class c (must be a descendant of Node) with the xml-mapping framework.
A new “factory method” will automatically be added to ClassMethods (and therefore to all
classes that include XML::Mapping from now on);
so you can call it from the body of your mapping class definition in order
to create nodes of type c. The name of the factory method is
derived by “underscoring” the (unqualified) name of c; e.g.
c==Foo::Bar::MyNiftyNode
will result in the creation
of a factory method named my_nifty_node
. The generated factory
method creates and returns a new instance of c. The list of
argument to c.new consists of self (i.e. the mapping
class the factory method was called from) followed by the arguments passed
to the factory method. You should always use the factory methods to create
instances of node classes; you should never need to call a node class's
constructor directly.
For a demonstration, see the calls to text_node
,
array_node
etc. in the examples along with the corresponding
node classes TextNode, ArrayNode etc. (these predefined node
classes are in no way “special”; they're added using ::add_node_class in
mapping.rb just like any custom node classes would be).
# File lib/xml/mapping/base.rb, line 505 def self.add_node_class(c) meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase} ClassMethods.module_eval <<-EOS def #{meth_name}(*args) #{c.name}.new(self,*args) end EOS end
Finds a mapping class and mapping name corresponding to the given XML root element name. There may be more than one (class,mapping) tuple for a given root element name – in that case, one of them is selected arbitrarily.
returns [class,mapping]
# File lib/xml/mapping/base.rb, line 134 def self.class_and_mapping_for_root_elt_name(name) (Classes_by_rootelt_names[name] || {}).each_pair{|mapping,classes| return [classes[0],mapping] } nil end
Finds a mapping class corresponding to the given XML root element name and mapping name. There may be more than one such class – in that case, the most recently defined one is returned
This is the inverse operation to <class>.root_element_name (see XML::Mapping::ClassMethods#root_element_name).
# File lib/xml/mapping/base.rb, line 122 def self.class_for_root_elt_name(name, options={:mapping=>:_default}) # TODO: implement Hash read-only instead of this # interface Classes_by_rootelt_names.classes_for(name, options[:mapping])[-1] end
Like ::load_object_from_xml, but loads from the XML file named by filename.
# File lib/xml/mapping/base.rb, line 475 def self.load_object_from_file(filename,options={:mapping=>nil}) xml = REXML::Document.new(File.new(filename)) load_object_from_xml xml.root, options end
“polymorphic” load function. Turns the XML tree xml into an object, which is returned. The class of the object and the mapping to be used for unmarshalling are automatically determined from the root element name of xml using ::class_for_root_elt_name. If :mapping is non-nil, only root element names defined in that mapping will be considered (default is to consider all classes)
# File lib/xml/mapping/base.rb, line 461 def self.load_object_from_xml(xml,options={:mapping=>nil}) if mapping = options[:mapping] c = class_for_root_elt_name xml.name, :mapping=>mapping else c,mapping = class_and_mapping_for_root_elt_name(xml.name) end unless c raise MappingError, "no mapping class for root element name #{xml.name}, mapping #{mapping.inspect}" end c.load_from_xml xml, :mapping=>mapping end
Initializer. Called (by Class#new) after self was created using new.
XML::Mapping's implementation calls initialize_xml_mapping.
# File lib/xml/mapping/base.rb, line 169 def initialize(*args) super(*args) initialize_xml_mapping end
Public Instance Methods
“fill” the contents of xml into self. xml is a REXML::Element.
First, #pre_load(xml) is called, then all the nodes for this object's class are processed (i.e. have their xml_to_obj method called) in the order of their definition inside the class, then post_load is called.
# File lib/xml/mapping/base.rb, line 181 def fill_from_xml(xml, options={:mapping=>:_default}) raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") unless self.class.xml_mapping_nodes_hash.has_key?(options[:mapping]) pre_load xml, :mapping=>options[:mapping] self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| node.xml_to_obj self, xml end post_load :mapping=>options[:mapping] end
Fill self's state into the xml node (REXML::Element) xml. All the nodes for this object's class are processed (i.e. have their obj_to_xml method called) in the order of their definition inside the class.
# File lib/xml/mapping/base.rb, line 216 def fill_into_xml(xml, options={:mapping=>:_default}) self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| node.obj_to_xml self,xml end end
Xml-mapping-specific initializer.
This will be called when a new instance is being initialized from an XML source, as well as after calling class.new(args) (for the latter case to work, you'll have to make sure you call the inherited initialize method)
The :mapping keyword argument gives the mapping the instance is being initialized with. This is non-nil only when the instance is being initialized from an XML source (:mapping will contain the :mapping argument passed (explicitly or implicitly) to the load_from_… method).
When the instance is being initialized because class.new was called, the :mapping argument is set to nil to show that the object is being initialized with respect to no specific mapping.
The default implementation of this method calls obj_initializing on all
nodes. You may overwrite this method to do your own initialization stuff;
make sure to call super
in that case.
# File lib/xml/mapping/base.rb, line 159 def initialize_xml_mapping(options={:mapping=>nil}) self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node| node.obj_initializing(self,options[:mapping]) end end
This method is called immediately after self has been filled from an xml source. If you have things to do after the object has been succefully loaded from the xml (reorganising the loaded data in some way, setting up additional views on the data etc.), this is the place where you put them. You can also raise an exception to abandon the whole loading process.
The default implementation of this method is empty.
# File lib/xml/mapping/base.rb, line 207 def post_load(options={:mapping=>:_default}) end
This method is called immediately after self's state has been filled into an XML element.
The default implementation does nothing.
# File lib/xml/mapping/base.rb, line 253 def post_save(xml, options={:mapping=>:_default}) end
This method is called immediately before self is filled from an xml source. xml is the source REXML::Element.
The default implementation of this method is empty.
# File lib/xml/mapping/base.rb, line 195 def pre_load(xml, options={:mapping=>:_default}) end
This method is called when self is to be converted to an XML tree. It must create and return an XML element (as a REXML::Element); that element will then be passed to fill_into_xml.
The default implementation of this method creates a new empty element whose name is the root_element_name of self's class (see XML::Mapping::ClassMethods#root_element_name). By default, this is the class name, with capital letters converted to lowercase and preceded by a dash, e.g. “MySampleClass” becomes “my-sample-class”.
# File lib/xml/mapping/base.rb, line 245 def pre_save(options={:mapping=>:_default}) REXML::Element.new(self.class.root_element_name(:mapping=>options[:mapping])) end
Save self's state as XML into the file named filename. The XML is obtained by calling save_to_xml.
# File lib/xml/mapping/base.rb, line 259 def save_to_file(filename, options={:mapping=>:_default}) xml = save_to_xml :mapping=>options[:mapping] formatter = options[:formatter] || self.class.mapping_output_formatter File.open(filename,"w") do |f| formatter.write(xml, f) end end
Fill self's state into a new xml node, return that node.
This method calls pre_save, then fill_into_xml, then post_save.
# File lib/xml/mapping/base.rb, line 227 def save_to_xml(options={:mapping=>:_default}) xml = pre_save :mapping=>options[:mapping] fill_into_xml xml, :mapping=>options[:mapping] post_save xml, :mapping=>options[:mapping] xml end