Additional mapping nodes and other utility code for working with XML::Mapping.
This gem adds two methods, write_xml
and parse_xml
, to XML mapping instances and classes respectively, to reduce
boilerplate.
The write_xml
method supplements XML::Mapping#save_to_xml
by writing the object out as a String
rather than as an REXML::Element
.
elem = MyElement.new
elem.attribute = 123
elem.text = 'element text'
elem.children = ['child 1', 'child 2']
puts elem.write_xml
outputs
<my-element attribute='123'>
element text
<child>child 1</child>
<child>child 2</child>
</my-element>
The parse_xml
method supplements
XML::Mapping::ClassMethods#load_from_xml
by abstracting away the difference between strings, XML documents, XML elements,
files, and IO-like objects.
my_xml_path = 'my_xml.xml'
my_xml_file = File.new(my_xml_path)
my_xml_string = File.read(my_xml_path)
my_xml_io = StringIO.new(my_xml_string)
my_xml_document = REXML::Document.new(my_xml_string)
my_xml_element = my_xml_document.root
# Standard XML::Mapping load_from_xml method
elem = MyXMLClass.load_from_xml(my_xml_element)
# parse_xml equivalent
[my_xml_file, my_xml_string, my_xml_io, my_xml_document, my_xml_element].each do |xml_source|
expect(MyXMLClass.parse_xml(xml_source)).to eq(elem) # assuming MyXMLClass implements ==
end
Both write_xml
and parse_xml
accept an options
hash, to be passed on to save_to_xml
or load_from_xml
,
respectively:
elem = MyXMLClass.parse(my_xml_string, { mapping: :alternate })
new_xml_string = elem.write_xml({ mapping: :alternate })
To create a custom node type, require xml/mapping_extensions
and extend one of
the abstract node classes, or use one of the provided implementations.
NodeBase
: Base class for simple single-attribute nodes that convert XML strings to object values.
Note that you must call ::XML::Mapping.add_node_class
for your new node class
to be registered with the XML mapping engine.
class LaTeXRationalNode < XML::MappingExtensions::NodeBase
def to_value(xml_text)
match_data = /\\frac\{([0-9.]+)\}\{([0-9.]+)\}/.match(xml_text)
Rational("#{match_data[1]}/#{match_data[2]}")
end
def to_xml_text(value)
"\\frac{#{value.numerator}}{#{value.denominator}}"
end
end
XML::Mapping.add_node_class LaTeXRationalNode
DateNode
: maps XML Schema dates toDate
objectsTimeNode
: ISO 8601 strings toTime
objectsUriNode
: maps URI strings toURI
objectsMimeTypeNode
: maps MIME type strings toMIME::Type
objectsTypesafeEnumNode
: maps XML strings to typesafe_enum values
require 'xml/mapping_extensions'
require 'rexml/document'
class MyElem
include ::XML::Mapping
root_element_name 'my_elem'
date_node :plain_date, 'plain_date'
date_node :zulu_date, 'zulu_date', zulu: true
time_node :time, 'time'
uri_node :uri, 'uri'
mime_type_node :mime_type, 'mime_type'
end
xml_str = <<-XML
<my_elem>
<plain_date>1999-12-31</plain_date>
<zulu_date>2000-01-01Z</zulu_date>
<time>2000-01-01T02:34:56Z</time>
<uri>http://example.org</uri>
<mime_type>text/plain</mime_type>
</my_elem>
XML
xml_doc = REXML::Document.new(xml_str)
xml_elem = xml_doc.root
elem = MyElem.load_from_xml(xml_elem)
puts elem.plain_date.inspect
puts elem.zulu_date.inspect
puts elem.time.inspect
puts elem.uri.inspect
puts elem.mime_type.inspect
Outputs
#<Date: 1999-12-31 ((2451544j,0s,0n),+0s,2299161j)>
#<Date: 2000-01-01 ((2451545j,0s,0n),+0s,2299161j)>
2000-01-01 02:34:56 UTC
#<URI::HTTP http://example.org>
#<MIME::Type:0x007f864bdc4f78 @friendly={"en"=>"Text File"}, @system=nil, @obsolete=false, @registered=true, @use_instead=nil, @signature=false, @content_type="text/plain", @raw_media_type="text", @raw_sub_type="plain", @simplified="text/plain", @i18n_key="text.plain", @media_type="text", @sub_type="plain", @docs=[], @encoding="quoted-printable", @extensions=["txt", "asc", "c", "cc", "h", "hh", "cpp", "hpp", "dat", "hlp", "conf", "def", "doc", "in", "list", "log", "markdown", "md", "rst", "text", "textile"], @references=["IANA", "RFC2046", "RFC3676", "RFC5147"], @xrefs={"rfc"=>["rfc2046", "rfc3676", "rfc5147"]}>
elem = MyElem.new
elem.plain_date = Date.new(1999, 12, 31)
elem.zulu_date = Date.new(2000, 1, 1)
elem.time = Time.utc(2000, 1, 1, 2, 34, 56)
elem.uri = URI('http://example.org')
elem.mime_type = MIME::Types['text/plain'].first
puts(elem.write_xml)
Outputs:
<my_elem>
<plain_date>1999-12-31</plain_date>
<zulu_date>2000-01-01Z</zulu_date>
<time>2000-01-01T02:34:56Z</time>
<uri>http://example.org</uri>
<mime_type>text/plain</mime_type>
</my_elem>
The Namespace
class encapsulates an XML namespace. The Namespaced
module extends XML::Mapping
to
add a namespace
attribute and write the namespace out when saving to XML.
class MyElem
include XML::MappingExtensions::Namespaced # instead of XML::Mapping
namespace Namespace.new(
prefix: 'px',
uri: 'http://example.org/px'
)
root_element_name 'my_elem'
date_node :plain_date, 'plain_date'
date_node :zulu_date, 'zulu_date', zulu: true
time_node :time, 'time'
uri_node :uri, 'uri'
mime_type_node :mime_type, 'mime_type'
end
MyElem.namespace
# => #<XML::MappingExtensions::Namespace:0x007fb1c6b73e80>
The namespace will then be written out when the object is saved to XML:
obj = MyElem.new(...)
obj.namespace = namespace
puts obj.write_xml
<element
xmlns='http://example.org/px/'
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:schemaLocation='http://example.org/px.xsd'
attribute='123'>
element text
<child>child 1</child>
<child>child 2</child>
</element>
Setting a prefix
attribute on the namespace will set the prefix on each element in the output:
class MyElem
namespace Namespace.new(
prefix: 'px',
uri: 'http://example.org/px',
schema_location: 'http://example.org/px.xsd'
)
end
obj = MyElem.new(...)
obj.namespace = namespace
puts obj.write_xml
<px:element
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:schemaLocation='http://example.org/px.xsd'
xmlns:px='http://example.org/px/'
attribute='123'>
element text
<px:child>child 1</px:child>
<px:child>child 2</px:child>
</px:element>
The XML::Mapping library provides an “alternate mapping” mechanism allowing multiple XML mappings per class. However, it requires each mapping to be exhaustive -- an alternate mapping must redefine all attribute mappings defined in the primary, or else ignore those attributes. Sometimes, however, it is
class ValidatingElement
include ::XML::Mapping
root_element_name 'element'
text_node :name, '@name'
text_node :value, '@value'
use_mapping :strict
numeric_node :value, '@value', writer: proc { |obj, xml| xml.add_attribute('value', Float(obj.value)) }
end
invalid_string = '<element name="abc" value="efg"/>'
ValidatingElement.parse_xml(invalid_string, mapping: :strict)
# ArgumentError: invalid value for Float(): "efg"
elem = ValidatingElement.parse_xml(invalid_string) # OK
# => #<XML::Mapping::ValidatingElement:0x007fa5631ae9c8>
elem.write_xml
# => "<element name='abc' value='efg'/>"
elem.write_xml(mapping: :strict)
# ArgumentError: invalid value for Float(): "efg"
So far, so good; but say we set a valid value and try to output with the
:strict
mapping?
elem.value = 123
elem.write_xml(mapping: :strict)
# => <validating-element value='123'/>
Since the :strict
mapping doesn't define a root element name, we get the
default (based on the class name), and since it doesn't define a mapping
for the name
attribute, we lose that entirely.
But if we add a fallback mapping --
class ValidatingElement
fallback_mapping :strict, :_default
end
elem.write_xml(mapping: :strict)
# => <element value='123' name='abc'/>
-- the :strict
mapping now gets the root element name element
,
and the name
attribute, as defined under the :_default
mapping.