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

add_node_class(c) click to toggle source

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
class_and_mapping_for_root_elt_name(name) click to toggle source

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
class_for_root_elt_name(name, options={:mapping=>:_default}) click to toggle source

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
load_object_from_file(filename,options={:mapping=>nil}) click to toggle source

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
load_object_from_xml(xml,options={:mapping=>nil}) click to toggle source

“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
new(*args) click to toggle source

Initializer. Called (by Class#new) after self was created using new.

XML::Mapping's implementation calls initialize_xml_mapping.

Calls superclass method
# File lib/xml/mapping/base.rb, line 169
def initialize(*args)
  super(*args)
  initialize_xml_mapping
end

Public Instance Methods

fill_from_xml(xml, options={:mapping=>:_default}) click to toggle source

“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_into_xml(xml, options={:mapping=>:_default}) click to toggle source

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
initialize_xml_mapping(options={:mapping=>nil}) click to toggle source

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
post_load(options={:mapping=>:_default}) click to toggle source

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
post_save(xml, options={:mapping=>:_default}) click to toggle source

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
pre_load(xml, options={:mapping=>:_default}) click to toggle source

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
pre_save(options={:mapping=>:_default}) click to toggle source

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_to_file(filename, options={:mapping=>:_default}) click to toggle source

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
save_to_xml(options={:mapping=>:_default}) click to toggle source

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