Module: XML::Mapping

Defined in:
lib/xml/mapping/base.rb,
lib/xml/mapping/version.rb,
lib/xml/mapping/standard_nodes.rb

Overview

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:

:include: company.xml

mapping class declaration:

:include: company.rb

usage:

:include: company_usage.intout

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 XML::Mapping.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.

Defined Under Namespace

Modules: ClassMethods Classes: ArrayNode, BooleanNode, ChoiceNode, HashNode, Node, NumericNode, ObjectNode, SingleAttributeNode, SubObjectBaseNode, TextNode

Constant Summary collapse

Classes_by_rootelt_names =

defined mapping classes for a given root elt name and mapping name (nested map from root element name to mapping name to array of classes)

can’t really use a class variable for this because it must be shared by all class methods mixed into classes by including Mapping. See multi-io.github.io/mydocs-pub/ruby/mixin_class_methods_global_state.txt.html for a more detailed discussion.

{}
VERSION =
'0.10.1'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.add_node_class(c) ⇒ Object

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).



505
506
507
508
509
510
511
512
# 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 "    def \#{meth_name}(*args)\n      \#{c.name}.new(self,*args)\n    end\n  EOS\nend\n"

.append_features(base) ⇒ Object

:nodoc:



108
109
110
111
112
113
# File 'lib/xml/mapping/base.rb', line 108

def self.append_features(base) #:nodoc:
  super
  base.extend(ClassMethods)
  Classes_by_rootelt_names.create_classes_for(base.default_root_element_name, :_default) << base
  base.initializing_xml_mapping
end

.class_and_mapping_for_root_elt_name(name) ⇒ Object

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]



134
135
136
137
# 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}) ⇒ Object

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).



122
123
124
125
126
# 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}) ⇒ Object

Like load_object_from_xml, but loads from the XML file named by filename.



475
476
477
478
# 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}) ⇒ Object

“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 XML::Mapping.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)



461
462
463
464
465
466
467
468
469
470
471
# 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

Instance Method Details

#fill_from_xml(xml, options = {:mapping=>:_default}) ⇒ Object

“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.

Raises:



181
182
183
184
185
186
187
188
189
# 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}) ⇒ Object

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.



216
217
218
219
220
# 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(*args) ⇒ Object

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

XML::Mapping’s implementation calls #initialize_xml_mapping.



169
170
171
172
# File 'lib/xml/mapping/base.rb', line 169

def initialize(*args)
  super(*args)
  initialize_xml_mapping
end

#initialize_xml_mapping(options = {:mapping=>nil}) ⇒ Object

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.



159
160
161
162
163
# 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}) ⇒ Object

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.



207
208
# File 'lib/xml/mapping/base.rb', line 207

def post_load(options={:mapping=>:_default})
end

#post_save(xml, options = {:mapping=>:_default}) ⇒ Object

This method is called immediately after self’s state has been filled into an XML element.

The default implementation does nothing.



253
254
# File 'lib/xml/mapping/base.rb', line 253

def post_save(xml, options={:mapping=>:_default})
end

#pre_load(xml, options = {:mapping=>:_default}) ⇒ Object

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.



195
196
# File 'lib/xml/mapping/base.rb', line 195

def pre_load(xml, options={:mapping=>:_default})
end

#pre_save(options = {:mapping=>:_default}) ⇒ Object

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 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”.



245
246
247
# 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}) ⇒ Object

Save self’s state as XML into the file named filename. The XML is obtained by calling #save_to_xml.



259
260
261
262
263
264
265
# 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}) ⇒ Object

Fill self’s state into a new xml node, return that node.

This method calls #pre_save, then #fill_into_xml, then #post_save.



227
228
229
230
231
232
# 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