# xml-mapping -- bidirectional Ruby-XML mapper
# Copyright (C) 2004,2005 Olaf Klischat
module XML
module Mapping
# Node factory function synopsis:
#
# text_node :_attrname_, _path_ [, :default_value=>_obj_]
# [, :optional=>true]
#
# Node that maps an XML node's text (the element's first child
# text node resp. the attribute's value) to a (string) attribute
# of the mapped object. Since TextNode inherits from
# SingleAttributeNode, the first argument to the node factory
# function is the attribute name (as a symbol). Handling of
# :default_value and :optional option arguments
# (if given) is also provided by the superclass -- see there for
# details.
class TextNode < SingleAttributeNode
# Initializer. _path_ (a string, the 2nd argument to the node
# factory function) is the XPath expression that locates the
# mapped node in the XML.
def initialize_impl(path)
@path = XML::XXPath.new(path)
end
def extract_attr_value(xml) # :nodoc:
default_when_xpath_err{ @path.first(xml).text }
end
def set_attr_value(xml, value) # :nodoc:
@path.first(xml,:ensure_created=>true).text = value
end
end
# Node factory function synopsis:
#
# numeric_node :_attrname_, _path_ [, :default_value=>_obj_]
# [, :optional=>true]
#
# Like TextNode, but interprets the XML node's text as a number
# (Integer or Float, depending on the nodes's text) and maps it to
# an Integer or Float attribute.
class NumericNode < SingleAttributeNode
def initialize_impl(path)
@path = XML::XXPath.new(path)
end
def extract_attr_value(xml) # :nodoc:
txt = default_when_xpath_err{ @path.first(xml).text }
begin
Integer(txt)
rescue ArgumentError
Float(txt)
end
end
def set_attr_value(xml, value) # :nodoc:
raise RuntimeError, "Not an integer: #{value}" unless Numeric===value
@path.first(xml,:ensure_created=>true).text = value.to_s
end
end
# (does somebody have a better name for this class?) base node
# class that provides an initializer which lets the user specify a
# means to marshal/unmarshal a Ruby object to/from XML. Used as
# the base class for nodes that map some sub-nodes of their XML
# tree to (Ruby-)sub-objects of their attribute.
class SubObjectBaseNode < SingleAttributeNode
# processes the keyword arguments :class, :marshaller, and
# :unmarshaller (_args_ is ignored). When this initiaizer
# returns, @options[:marshaller] and @options[:unmarshaller] are
# set to procs that marshal/unmarshal a Ruby object to/from an
# XML tree according to the keyword arguments that were passed
# to the initializer:
#
# You either supply a :class argument with a class implementing
# XML::Mapping -- in that case, the subtree will be mapped to an
# instance of that class (using load_from_xml
# resp. fill_into_xml). Or, you supply :marshaller and
# :unmarshaller arguments specifying explicit
# unmarshaller/marshaller procs. The :marshaller proc takes
# arguments _xml_,_value_ and must fill _value_ (the object to
# be marshalled) into _xml_; the :unmarshaller proc takes _xml_
# and must extract and return the object value from it. Or, you
# specify none of those arguments, in which case the name of the
# class to create will be automatically deduced from the root
# element name of the XML node (see
# XML::Mapping::load_object_from_xml,
# XML::Mapping::class_for_root_elt_name).
#
# If both :class and :marshaller/:unmarshaller arguments are
# supplied, the latter take precedence.
def initialize_impl(*args)
if @options[:class]
unless @options[:marshaller]
@options[:marshaller] = proc {|xml,value|
value.fill_into_xml(xml)
}
end
unless @options[:unmarshaller]
@options[:unmarshaller] = proc {|xml|
@options[:class].load_from_xml(xml)
}
end
end
unless @options[:marshaller]
@options[:marshaller] = proc {|xml,value|
value.fill_into_xml(xml)
if xml.unspecified?
xml.name = value.class.root_element_name
xml.unspecified = false
end
}
end
unless @options[:unmarshaller]
@options[:unmarshaller] = proc {|xml|
XML::Mapping.load_object_from_xml(xml)
}
end
end
end
# Node factory function synopsis:
#
# object_node :_attrname_, _path_ [, :default_value=>_obj_]
# [, :optional=>true]
# [, :class=>_c_]
# [, :marshaller=>_proc_]
# [, :unmarshaller=>_proc_]
#
# Node that maps a subtree in the source XML to a Ruby
# object. :_attrname_ and _path_ are again the attribute name
# resp. XPath expression of the mapped attribute; the keyword
# arguments :default_value and :optional are
# handled by the SingleAttributeNode superclass. The XML subnode
# named by _path_ is mapped to the attribute named by :_attrname_
# according to the keyword arguments :class,
# :marshaller, and :unmarshaller, which are
# handled by the SubObjectBaseNode superclass.
class ObjectNode < SubObjectBaseNode
# Initializer. _path_ (a string denoting an XPath expression) is
# the location of the subtree.
def initialize_impl(path)
super
@path = XML::XXPath.new(path)
end
def extract_attr_value(xml) # :nodoc:
@options[:unmarshaller].call(default_when_xpath_err{@path.first(xml)})
end
def set_attr_value(xml, value) # :nodoc:
@options[:marshaller].call(@path.first(xml,:ensure_created=>true), value)
end
end
# Node factory function synopsis:
#
# boolean_node :_attrname_, _path_,
# _true_value_, _false_value_ [, :default_value=>_obj_]
# [, :optional=>true]
#
# Node that maps an XML node's text (the element name resp. the
# attribute value) to a boolean attribute of the mapped
# object. The attribute named by :_attrname_ is mapped to/from the
# XML subnode named by the XPath expression _path_. _true_value_
# is the text the node must have in order to represent the +true+
# boolean value, _false_value_ (actually, any value other than
# _true_value_) is the text the node must have in order to
# represent the +false+ boolean value.
class BooleanNode < SingleAttributeNode
# Initializer.
def initialize_impl(path,true_value,false_value)
@path = XML::XXPath.new(path)
@true_value = true_value; @false_value = false_value
end
def extract_attr_value(xml) # :nodoc:
default_when_xpath_err{ @path.first(xml).text==@true_value }
end
def set_attr_value(xml, value) # :nodoc:
@path.first(xml,:ensure_created=>true).text = value ? @true_value : @false_value
end
end
# Node factory function synopsis:
#
# array_node :_attrname_, _per_arrelement_path_
# [, :default_value=>_obj_]
# [, :optional=>true]
# [, :class=>_c_]
# [, :marshaller=>_proc_]
# [, :unmarshaller=>_proc_]
#
# -or-
#
# array_node :_attrname_, _base_path_, _per_arrelement_path_
# [keyword args the same]
#
# Node that maps a sequence of sub-nodes of the XML tree to an
# attribute containing an array of Ruby objects, with each array
# element mapping to a corresponding member of the sequence of
# sub-nodes.
#
# If _base_path_ is not supplied, it is assumed to be
# "". _base_path_+"/"+_per_arrelement_path_ is an XPath
# expression that must "yield" the sequence of XML nodes that is
# to be mapped to the array. The difference between _base_path_
# and _per_arrelement_path_ becomes important when marshalling the
# array attribute back to XML. When that happens, _base_path_
# names the most specific common parent node of all the mapped
# sub-nodes, and _per_arrelement_path_ names (relative to
# _base_path_) the part of the path that is duplicated for each
# array element. For example, with _base_path_=="foo/bar"
# and _per_arrelement_path_=="hi/ho", an array
# [x,y,z] will be written to an XML structure that looks
# like this:
#
#
#
#
#
# [marshalled object x]
#
#
#
#
# [marshalled object y]
#
#
#
#
# [marshalled object z]
#
#
#
#
class ArrayNode < SubObjectBaseNode
# Initializer, delegates to do_initialize. Called with keyword
# arguments and either 1 or 2 paths; the hindmost path argument
# passed is delegated to _per_arrelement_path_; the preceding
# path argument (if present, "" by default) is delegated to
# _base_path_.
def initialize_impl(path,path2=nil)
super
if path2
do_initialize(path,path2)
else
do_initialize("",path)
end
end
# "Real" initializer.
def do_initialize(base_path,per_arrelement_path)
per_arrelement_path=per_arrelement_path[1..-1] if per_arrelement_path[0]==?/
@base_path = XML::XXPath.new(base_path)
@per_arrelement_path = XML::XXPath.new(per_arrelement_path)
@reader_path = XML::XXPath.new(base_path+"/"+per_arrelement_path)
end
def extract_attr_value(xml) # :nodoc:
result = []
default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
result << @options[:unmarshaller].call(elt)
end
result
end
def set_attr_value(xml, value) # :nodoc:
base_elt = @base_path.first(xml,:ensure_created=>true)
value.each do |arr_elt|
@options[:marshaller].call(@per_arrelement_path.create_new(base_elt), arr_elt)
end
end
end
# Node factory function synopsis:
#
# hash_node :_attrname_, _per_hashelement_path_, _key_path_
# [, :default_value=>_obj_]
# [, :optional=>true]
# [, :class=>_c_]
# [, :marshaller=>_proc_]
# [, :unmarshaller=>_proc_]
#
# - or -
#
# hash_node :_attrname_, _base_path_, _per_hashelement_path_, _key_path_
# [keyword args the same]
#
# Node that maps a sequence of sub-nodes of the XML tree to an
# attribute containing a hash of Ruby objects, with each hash
# value mapping to a corresponding member of the sequence of
# sub-nodes. The (string-valued) hash key associated with a hash
# value _v_ is mapped to the text of a specific sub-node of _v_'s
# sub-node.
#
# Analogously to ArrayNode, _base_path_ and _per_arrelement_path_
# define the XPath expression that "yields" the sequence of XML
# nodes, each of which maps to a value in the hash table. Relative
# to such a node, key_path_ names the node whose text becomes the
# associated hash key.
class HashNode < SubObjectBaseNode
# Initializer, delegates to do_initialize. Called with keyword
# arguments and either 2 or 3 paths; the hindmost path argument
# passed is delegated to _key_path_, the preceding path argument
# is delegated to _per_arrelement_path_, the path preceding that
# argument (if present, "" by default) is delegated to
# _base_path_. The meaning of the keyword arguments is the same
# as for ObjectNode.
def initialize_impl(path1,path2,path3=nil)
super
if path3
do_initialize(path1,path2,path3)
else
do_initialize("",path1,path2)
end
end
# "Real" initializer.
def do_initialize(base_path,per_hashelement_path,key_path)
per_hashelement_path=per_hashelement_path[1..-1] if per_hashelement_path[0]==?/
@base_path = XML::XXPath.new(base_path)
@per_hashelement_path = XML::XXPath.new(per_hashelement_path)
@key_path = XML::XXPath.new(key_path)
@reader_path = XML::XXPath.new(base_path+"/"+per_hashelement_path)
end
def extract_attr_value(xml) # :nodoc:
result = {}
default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
key = @key_path.first(elt).text
value = @options[:unmarshaller].call(elt)
result[key] = value
end
result
end
def set_attr_value(xml, value) # :nodoc:
base_elt = @base_path.first(xml,:ensure_created=>true)
value.each_pair do |k,v|
elt = @per_hashelement_path.create_new(base_elt)
@options[:marshaller].call(elt,v)
@key_path.first(elt,:ensure_created=>true).text = k
end
end
end
end
end