diff --git a/.gitignore b/.gitignore deleted file mode 100644 index decca8f..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*~ -*.intout -*.lock -doc/api -user_manual.md -user_manual_xxpath.md -pkg/ \ No newline at end of file diff --git a/Kernel.html b/Kernel.html new file mode 100644 index 0000000..1655db0 --- /dev/null +++ b/Kernel.html @@ -0,0 +1,185 @@ + + + + + + +module Kernel - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module Kernel +

+ +
+ +

unsuppress

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ old_warn(msg) + +
+ + +
+ + + + + + +
+ + +
+ Also aliased as: warn +
+ + + +
+ Alias for: warn +
+ +
+ + +
+ +
+ warn(msg) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 155
+def warn(msg)
+end
+
+ +
+ + +
+ Also aliased as: old_warn +
+ + + +
+ + +
+ +
+
+ + + + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 86030ea..0000000 --- a/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2014 Olaf Klischat - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/Numeric.html b/Numeric.html new file mode 100644 index 0000000..c2e3637 --- /dev/null +++ b/Numeric.html @@ -0,0 +1,235 @@ + + + + + + +class Numeric - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class Numeric +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ load_from_xml(xml, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 17
+def self.load_from_xml(xml, options={:mapping=>:_default})
+  begin
+    Integer(xml.text)
+  rescue ArgumentError
+    Float(xml.text)
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ fill_into_xml(xml, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 25
+def fill_into_xml(xml, options={:mapping=>:_default})
+  xml.text = self.to_s
+end
+
+ +
+ + + + +
+ + +
+ +
+ text() + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 29
+def text
+  self.to_s
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/README.md b/README.md deleted file mode 100644 index 67bf4f0..0000000 --- a/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter - -Xml-mapping is an easy to use, extensible library that allows you to -semi-automatically map Ruby objects to XML trees and vice versa. - - -## Trivial Example - -### sample document - - - - - Stuffed Penguin - 10 - 8.95 - - -### mapping class - - class Item - include XML::Mapping - - text_node :ref, "@reference" - text_node :descr, "Description" - numeric_node :quantity, "Quantity" - numeric_node :unit_price, "UnitPrice" - - def total_price - quantity*unit_price - end - end - - -### usage - - i = Item.load_from_file("item.xml") - => # - - i.unit_price = 42.23 - xml=i.save_to_xml #convert to REXML node; there's also o.save_to_file(name) - xml.write($stdout,2) - - - Stuffed Penguin - 10 - 42.23 - - - - -This is the most trivial example -- the mapper supports arbitrary -array and hash (map) nodes, object (reference) nodes and arrays/hashes -of those, polymorphic mappings, multiple mappings per class, fully -programmable mappings and arbitrary user-defined node types. Read the -[project documentation](http://multi-io.github.io/xml-mapping/ -"Project Page") for more information. diff --git a/README_md.html b/README_md.html new file mode 100644 index 0000000..68edfd0 --- /dev/null +++ b/README_md.html @@ -0,0 +1,155 @@ + + + + + + +README - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ +

XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter

+ +

Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.

+ +

Trivial Example

+ +

sample document

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<item reference="RF-0001">
+    <Description>Stuffed Penguin</Description>
+    <Quantity>10</Quantity>
+    <UnitPrice>8.95</UnitPrice>
+</item>
+ +

mapping class

+ +
class Item
+  include XML::Mapping
+
+  text_node :ref, "@reference"
+  text_node :descr, "Description"
+  numeric_node :quantity, "Quantity"
+  numeric_node :unit_price, "UnitPrice"
+
+  def total_price
+    quantity*unit_price
+  end
+end
+
+ +

usage

+ +
i = Item.load_from_file("item.xml")
+=> #<Item:0xb7888c90 @ref="RF-0001" @quantity=10, @descr="Stuffed Penguin", @unit_price=8.95>
+
+i.unit_price = 42.23
+xml=i.save_to_xml #convert to REXML node; there's also o.save_to_file(name)
+xml.write($stdout,2)
+
+<item reference="RF-0001">
+    <Description>Stuffed Penguin</Description>
+    <Quantity>10</Quantity>
+    <UnitPrice>42.23</UnitPrice>
+</item>
+ +

This is the most trivial example – the mapper supports arbitrary array and +hash (map) nodes, object (reference) nodes and arrays/hashes of those, +polymorphic mappings, multiple mappings per class, fully programmable +mappings and arbitrary user-defined node types. Read the project documentation for +more information.

+
+ + + + + diff --git a/REXML.html b/REXML.html new file mode 100644 index 0000000..043493e --- /dev/null +++ b/REXML.html @@ -0,0 +1,95 @@ + + + + + + +module REXML - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module REXML +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/REXML/Child.html b/REXML/Child.html new file mode 100644 index 0000000..2e873d4 --- /dev/null +++ b/REXML/Child.html @@ -0,0 +1,113 @@ + + + + + + +class REXML::Child - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class REXML::Child +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/REXML/Element.html b/REXML/Element.html new file mode 100644 index 0000000..71b4920 --- /dev/null +++ b/REXML/Element.html @@ -0,0 +1,166 @@ + + + + + + +class REXML::Element - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class REXML::Element +

+ +
+ +

inline code snippets. TODO: switch to REXML::Formatters there sometime.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Instance Methods

+
+ + +
+ +
+ write(output=$stdout, indent=-1, transitive=false, ie_hack=false) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 171
+def write(output=$stdout, indent=-1, transitive=false, ie_hack=false)
+  Kernel.warn("#{self.class.name}.write is deprecated.  See REXML::Formatters")
+  formatter = if indent > -1
+                if transitive
+                  require "rexml/formatters/transitive"
+                  REXML::Formatters::Transitive.new( indent, ie_hack )
+                else
+                  REXML::Formatters::Pretty.new( indent, ie_hack )
+                end
+              else
+                REXML::Formatters::Default.new( ie_hack )
+              end
+  formatter.write( self, output )
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/REXML/Parent.html b/REXML/Parent.html new file mode 100644 index 0000000..457af4a --- /dev/null +++ b/REXML/Parent.html @@ -0,0 +1,270 @@ + + + + + + +class REXML::Parent - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class REXML::Parent +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Instance Methods

+
+ + +
+ +
+ each_on_axis(axis, &block) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 142
+def each_on_axis(axis, &block)
+  send :"each_on_axis_#{axis}", &block
+end
+
+ +
+ + + + +
+ + +
+ +
+ each_on_axis_child() { |attribute| ... } + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 118
+def each_on_axis_child
+  if respond_to? :attributes
+    attributes.each_key do |name|
+      yield XML::XXPath::Accessors::Attribute.new(self, name, false)
+    end
+  end
+  each_child do |c|
+    yield c
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+ each_on_axis_descendant(&block) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 129
+def each_on_axis_descendant(&block)
+  each_on_axis_child do |c|
+    block.call c
+    if REXML::Parent===c
+      c.each_on_axis_descendant(&block)
+    end
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+ each_on_axis_self() { |self| ... } + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 138
+def each_on_axis_self
+  yield self
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 63f5cc1..0000000 --- a/Rakefile +++ /dev/null @@ -1,211 +0,0 @@ -# -*- ruby -*- -# adapted from active_record's Rakefile - -$:.unshift "." - -require 'rubygems' -require 'rake' -require 'rake/clean' -require 'rake/testtask' -require 'rdoc/task' -require 'rake/packagetask' -require 'rubygems/package_task' - -require File.dirname(__FILE__)+"/lib/xml/mapping/version" - -FILE_RDOC_MAIN = 'user_manual.md' -FILES_RDOC_EXTRA = [FILE_RDOC_MAIN] + %w{README.md user_manual_xxpath.md TODO.txt doc/xpath_impl_notes.txt} -FILES_RDOC_INCLUDES=`git ls-files examples`.split("\n").map{|f| f.gsub(/.intin.rb$/, '.intout')} - - -desc "Default Task" -task :default => [ :test ] - -Rake::TestTask.new :test do |t| - t.test_files = ["test/all_tests.rb"] - t.verbose = true -# t.loader = :testrb -end - -# runs tests only if sources have changed since last succesful run of -# tests -file "test_run" => FileList.new('lib/**/*.rb','test/**/*.rb') do - Task[:test].invoke - touch "test_run" -end - - - -RDoc::Task.new do |rdoc| - rdoc.rdoc_dir = 'doc/api' - rdoc.title = "XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper" - rdoc.options += %w{--line-numbers --include examples} - rdoc.main = FILE_RDOC_MAIN - rdoc.rdoc_files.include(*FILES_RDOC_EXTRA) - rdoc.rdoc_files.include('lib/**/*.rb') - - task :rdoc => (FileList.new("examples/**/*.rb") + FILES_RDOC_INCLUDES) -end - - -## need to process :include: statements manually so we can -## have the resulting markdown in the gem -### can't use a rule (recursion issues) -%w{user_manual.md user_manual_xxpath.md}.each do |out_name| - in_name = "#{File.basename(out_name,'.md')}.in.md" - CLEAN << out_name - file out_name => in_name do - begin - File.open(out_name, "w") do |fout| - File.open(in_name, "r") do |fin| - fin.each_line do |l| - if m=l.match(/:include: (.*)/) - File.open("examples/#{m[1]}") do |fincluded| - fincluded.each_line do |linc| - fout.puts " #{linc}" - end - end - else - fout.write l - end - end - end - end - rescue Exception - File.delete out_name - raise - end - end -end - - -#rule '.intout' => ['.intin.rb', *FileList.new("lib/**/*.rb")] do |task| # doesn't work -- see below -rule '.intout' => ['.intin.rb'] do |task| - this_file_re = Regexp.compile(Regexp.quote(__FILE__)) - b = binding - visible=true; visible_retval=true; handle_exceptions=false - old_stdout = $stdout - old_wd = Dir.pwd - begin - File.open(task.name,"w") do |fout| - $stdout = fout - File.open(task.source,"r") do |fin| - Dir.chdir File.dirname(task.name) - fin.read.split("#<=\n").each do |snippet| - - snippet.scan(/^#:(.*?):$/) do |switches| - case switches[0] - when "visible" - visible=true - when "invisible" - visible=false - when "visible_retval" - visible_retval=true - when "invisible_retval" - visible_retval=false - when "handle_exceptions" - handle_exceptions=true - when "no_exceptions" - handle_exceptions=false - end - end - snippet.gsub!(/^#:.*?:(?:\n|\z)/,'') - - print "#{snippet}\n" if visible - exc_handled = false - value = begin - eval(snippet,b) - rescue Exception - raise unless handle_exceptions - exc_handled = true - if visible - print "#{$!.class}: #{$!}\n" - for m in $@ - break if m=~this_file_re - print "\tfrom #{m}\n" - end - end - end - if visible and visible_retval and not exc_handled - print "=> #{value.inspect}\n" - end - end - end - end - rescue Exception - $stdout = old_stdout - Dir.chdir old_wd - File.delete task.name - raise - ensure - $stdout = old_stdout - Dir.chdir old_wd - end -end - -# have to add additional prerequisites manually because it appears -# that rules can only define a single prerequisite :-\ -FILES_RDOC_INCLUDES.select{|f|f=~/.intout$/}.each do |f| - CLEAN << f - file f => FileList.new("lib/**/*.rb") - file f => FileList.new("examples/**/*.rb") -end - -file 'examples/company_usage.intout' => ['examples/company.xml'] -file 'examples/documents_folders_usage.intout' => ['examples/documents_folders.xml'] -file 'examples/order_signature_enhanced_usage.intout' => ['examples/order_signature_enhanced.xml'] -file 'examples/order_usage.intout' => ['examples/order.xml'] -file 'examples/stringarray_usage.intout' => ['examples/stringarray.xml'] - - -spec = Gem::Specification.new do |s| - s.name = 'xml-mapping' - s.version = XML::Mapping::VERSION - s.platform = Gem::Platform::RUBY - s.summary = "XML-Object mapper for Ruby" - s.description = - "An easy to use, extensible library for semi-automatically mapping Ruby objects to XML and back. Includes an XPath interpreter." - s.files += FILES_RDOC_EXTRA - s.files += FILES_RDOC_INCLUDES - s.files += `git ls-files lib test`.split("\n") - s.files += %w{LICENSE Rakefile} - s.extra_rdoc_files = FILES_RDOC_EXTRA - s.rdoc_options += %w{--include examples} - s.require_path = 'lib' - s.add_development_dependency 'rake', '~> 0' - s.test_file = 'test/all_tests.rb' - s.author = 'Olaf Klischat' - s.email = 'olaf.klischat@gmail.com' - s.homepage = "https://github.com/multi-io/xml-mapping" - s.rubyforge_project = "xml-mapping" - s.licenses = ['Apache-2.0'] -end - - - -Gem::PackageTask.new(spec) do |p| - p.gem_spec = spec - p.need_tar = true - p.need_zip = true -end - - - -require 'tmpdir' - -def system_checked(*args) - system(*args) or raise "failed to run: #{args.inspect}" -end - -desc "updates gh-pages branch in the git with the latest rdoc" -task :ghpublish => [:rdoc] do - revision = `git rev-parse HEAD`.chomp - Dir.mktmpdir do |dir| - # --no-checkout also deletes all files in the target's index - system_checked("git clone --branch gh-pages --no-checkout . #{dir}") - cp_r FileList.new('doc/api/*'), dir - system_checked("cd #{dir} && git add . && git commit -m 'upgrade to #{revision}'") - system_checked("git fetch #{dir}") - system_checked("git branch -f gh-pages FETCH_HEAD") - end -end diff --git a/String.html b/String.html new file mode 100644 index 0000000..2ffb062 --- /dev/null +++ b/String.html @@ -0,0 +1,231 @@ + + + + + + +class String - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class String +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ load_from_xml(xml, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 2
+def self.load_from_xml(xml, options={:mapping=>:_default})
+  xml.text
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ fill_into_xml(xml, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 6
+def fill_into_xml(xml, options={:mapping=>:_default})
+  xml.text = self
+end
+
+ +
+ + + + +
+ + +
+ +
+ text() + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/core_classes_mapping.rb, line 10
+def text
+  self
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 2627477..0000000 --- a/TODO.txt +++ /dev/null @@ -1,75 +0,0 @@ -- consider switching from REXML to nokogiri and/or, maybe, ox. - -- XML::XXPath: Write a real XPath parser eventually - -- XML::XXPath: avoid duplicates in path.all(node) result arrays when - using the descendants ("//") axis - -- invent an XPath-like language for Ruby object graphs (i.e. a - language that is to Ruby object graphs what XPath is to XML - trees). Use expressions in that language as a generalization of - "attribute names" (e.g. the 1st parameter to single attribute node - factory methods). The language could more or less be Ruby itself, - but the write support would need some extra work... - -- XML::XXPath: - - - implement .[@attrname] steps - - - returns the context node iff it contains an attrname attribute - - - doesn't work properly in REXML::XPath? - - - implement *[@attrname] steps - - - implement *[@attrname='attrvalue'] steps - - - id/idref support (write support possible?) - -- XML::Mapping: make SubObjectBaseNode a mixin instead of a subclass - of SingleAttributeNode ("mapping sub-objects" and "mapping to a - single attribute" are orthogonal concepts; inheritance is bad design - here) - -- documentation: - - - consider switching to YARD - - - reasons: parameter/return type metadata, (maybe) plugin for the - code snippet inclusion stuff - - - user_manual: - - - document/show usage of default_when_xpath_err outside node type - implementations - - - user_manual_xxpath: - - - mention new step types, new axes, xml/xpath_methods - - -- XML::XXPath/XML::Mapping: support for XML namespaces in XML::XXPath - (match nodes with specific namespaces only) and XML::Mapping - (use_namespace etc.) - -- add streaming input/output to XML::Mapping, i.e. SAX-based input in - addition to the current REXML/DOM - based one. Probably won't be - implementable for some more complicated XPaths -- raise meaningful - exceptions in those cases. - - - would need support in xxpath - - - should probably be built on top of the Ruby 2.0 lazy enumeration - stuff - -- XML::XXPath/XML::Mapping: add XML text nodes (the sub-node of an - element node that contains that element's text) first-class to - XML::XXPath. Use it for things like text_node :contents, "text()". - - Along those lines: promote XPath node "unspecifiedness" from an - attribute to a REXML node object of "unspecified" class that's - turned into an attribute/element/text node when necessary - -- (eventually, maybe) provide a "scaffolding" feature to automatically - turn a dtd/schema into a set of node type definitions or even a set - of mapping classes diff --git a/TODO_txt.html b/TODO_txt.html new file mode 100644 index 0000000..0c3e5eb --- /dev/null +++ b/TODO_txt.html @@ -0,0 +1,171 @@ + + + + + + +TODO - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ +
+ + + + + diff --git a/XML.html b/XML.html new file mode 100644 index 0000000..4028d61 --- /dev/null +++ b/XML.html @@ -0,0 +1,107 @@ + + + + + + +module XML - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML +

+ +
+ +

xml-mapping – bidirectional Ruby-XML mapper

+ +
Copyright (C) 2004,2005 Olaf Klischat
+ +

xml-mapping – bidirectional Ruby-XML mapper

+ +
Copyright (C) 2004-2010 Olaf Klischat
+ +

xxpath – XPath implementation for Ruby, including write access

+ +
Copyright (C) 2004-2006 Olaf Klischat
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/Mapping.html b/XML/Mapping.html new file mode 100644 index 0000000..f6c20cf --- /dev/null +++ b/XML/Mapping.html @@ -0,0 +1,1008 @@ + + + + + + +module XML::Mapping - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ 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
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/ArrayNode.html b/XML/Mapping/ArrayNode.html new file mode 100644 index 0000000..0819d76 --- /dev/null +++ b/XML/Mapping/ArrayNode.html @@ -0,0 +1,227 @@ + + + + + + +class XML::Mapping::ArrayNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::ArrayNode +

+ +
+ +

Node factory function synopsis:

+ +
array_node :_attrname_, _per_arrelement_path_
+                  [, :default_value=>_obj_]
+                  [, :optional=>true]
+                  [, :class=>_c_]
+                  [, :marshaller=>_proc_]
+                  [, :unmarshaller=>_proc_]
+                  [, :mapping=>_m_]
+                  [, :sub_mapping=>_sm_]
+ +

-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:

+ +
<foo>
+ <bar>
+  <hi>
+   <ho>
+    [marshalled object x]
+   </ho>
+  </hi>
+  <hi>
+   <ho>
+    [marshalled object y]
+   </ho>
+  </hi>
+  <hi>
+   <ho>
+    [marshalled object z]
+   </ho>
+  </hi>
+ </bar>
+</foo>
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

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

+ + +
+ Calls superclass method + XML::Mapping::SubObjectBaseNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 270
+def initialize(*args)
+  path,path2,*args = super(*args)
+  base_path,per_arrelement_path = if path2
+                                    [path,path2]
+                                  else
+                                    [".",path]
+                                  end
+  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)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/BooleanNode.html b/XML/Mapping/BooleanNode.html new file mode 100644 index 0000000..7fc30e2 --- /dev/null +++ b/XML/Mapping/BooleanNode.html @@ -0,0 +1,178 @@ + + + + + + +class XML::Mapping::BooleanNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::BooleanNode +

+ +
+ +

Node factory function synopsis:

+ +
boolean_node :_attrname_, _path_,
+             _true_value_, _false_value_ [, :default_value=>_obj_]
+                                         [, :optional=>true]
+                                         [, :mapping=>_m_]
+ +

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.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

Initializer.

+ + +
+ Calls superclass method + XML::Mapping::SingleAttributeNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 197
+def initialize(*args)
+  path,true_value,false_value,*args = super(*args)
+  @path = XML::XXPath.new(path)
+  @true_value = true_value; @false_value = false_value
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/ChoiceNode.html b/XML/Mapping/ChoiceNode.html new file mode 100644 index 0000000..ca42653 --- /dev/null +++ b/XML/Mapping/ChoiceNode.html @@ -0,0 +1,350 @@ + + + + + + +class XML::Mapping::ChoiceNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::ChoiceNode +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ + + + +
+ Calls superclass method + XML::Mapping::Node.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 371
+def initialize(*args)
+  args = super(*args)
+  @choices = []
+  path=nil
+  args.each do |arg|
+    next if [:if,:then,:elsif].include? arg
+    if path.nil?
+      path = (if [:else,:default,:otherwise].include? arg
+                :else
+              else
+                XML::XXPath.new arg
+              end)
+    else
+      raise XML::MappingError, "node expected, found: #{arg.inspect}" unless Node===arg
+      @choices << [path,arg]
+
+      # undo what the node factory fcn did -- ugly ugly!  would
+      # need some way to lazy-evaluate arg (a proc would be
+      # simple but ugly for the user), and then use some
+      # mechanism (a flag with dynamic scope probably) to tell
+      # the node factory fcn not to add the node to the
+      # xml_mapping_nodes
+      @owner.xml_mapping_nodes(:mapping=>@mapping).delete arg 
+      path=nil
+    end
+  end
+
+  raise XML::MappingError, "node missing at end of argument list" unless path.nil?
+  raise XML::MappingError, "no choices were supplied" if @choices.empty?
+  
+  []
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ is_present_in?(obj) + + click to toggle source + +
+ + +
+ +

(overridden) true if at least one of our nodes is_present_in? obj.

+ + + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 432
+def is_present_in? obj
+  # TODO: use Enumerable#any?
+  @choices.inject(false){|prev,(path,node)| prev or node.is_present_in?(obj)}
+end
+
+ +
+ + + + +
+ + +
+ +
+ obj_initializing(obj,mapping) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 426
+def obj_initializing(obj,mapping)
+  @choices[0][1].obj_initializing(obj,mapping)
+end
+
+ +
+ + + + +
+ + +
+ +
+ obj_to_xml(obj,xml) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 414
+def obj_to_xml(obj,xml)
+  @choices.each do |path,node|
+    if node.is_present_in? obj
+      node.obj_to_xml(obj,xml)
+      path.first(xml, :ensure_created=>true)
+      return true
+    end
+  end
+  # @choices[0][1].obj_to_xml(obj,xml)
+  raise XML::MappingError, "obj_to_xml: no choice present in object: #{obj.inspect}"
+end
+
+ +
+ + + + +
+ + +
+ +
+ xml_to_obj(obj,xml) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 404
+def xml_to_obj(obj,xml)
+  @choices.each do |path,node|
+    if path==:else or not(path.all(xml).empty?)
+      node.xml_to_obj(obj,xml)
+      return true
+    end
+  end
+  raise XML::MappingError, "xml_to_obj: no choice matched in: #{xml}"
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/ClassMethods.html b/XML/Mapping/ClassMethods.html new file mode 100644 index 0000000..05ec947 --- /dev/null +++ b/XML/Mapping/ClassMethods.html @@ -0,0 +1,579 @@ + + + + + + +module XML::Mapping::ClassMethods - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::Mapping::ClassMethods +

+ +
+ +

The instance methods of this module are automatically added as class +methods to a class that includes XML::Mapping.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Instance Methods

+
+ + +
+ +
+ add_accessor(name) + + click to toggle source + +
+ + +
+ +

Add getter and setter methods for a new attribute named name (must +be a symbol or a string) to this class, taking care not to replace existing +getters/setters. This is a convenience method intended to be called from +Node class initializers.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 308
+def add_accessor(name)
+  # existing methods search. Search for symbols and strings
+  #  to be compatible with Ruby 1.8 and 1.9.
+  methods = self.instance_methods
+  if methods[0].kind_of? Symbol
+    getter = :"#{name}"
+    setter = :"#{name}="
+  else
+    getter = "#{name}"
+    setter = "#{name}="
+  end
+  unless methods.include?(getter)
+    self.module_eval <<-EOS
+      attr_reader :#{name}
+    EOS
+  end
+  unless methods.include?(setter)
+    self.module_eval <<-EOS
+      attr_writer :#{name}
+    EOS
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+ all_xml_mapping_nodes(options={:mapping=>nil,:create=>true}) + + click to toggle source + +
+ + +
+ +

enumeration of all nodes in effect when marshalling/unmarshalling this +class, that is, nodes defined for this class as well as for its +superclasses. The nodes are returned in the order of their definition, +starting with the topmost superclass that has nodes defined. keyword +arguments are the same as for xml_mapping_nodes.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 394
+def all_xml_mapping_nodes(options={:mapping=>nil,:create=>true})
+  # TODO: we could return a dynamic Enumerable here, or cache
+  # the array...
+  result = []
+  if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
+    result += superclass.all_xml_mapping_nodes options
+  end
+  result += xml_mapping_nodes options
+end
+
+ +
+ + + + +
+ + +
+ +
+ default_mapping() + + click to toggle source + +
+ + +
+ +

return the current default mapping (:_default initially, or the value set +with the latest call to #use_mapping)

+ + + + +
+
# File lib/xml/mapping/base.rb, line 300
+def default_mapping
+  @default_mapping
+end
+
+ +
+ + + + +
+ + +
+ +
+ default_root_element_name() + + click to toggle source + +
+ + +
+ +

The default root element name for this class. Equals the class name, with +all parent module names stripped, and with capital letters converted to +lowercase and preceded by a dash; e.g. “Foo::Bar::MySampleClass” becomes +“my-sample-class”.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 435
+def default_root_element_name
+  self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
+end
+
+ +
+ + + + +
+ + +
+ +
+ load_from_file(filename, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ +

Create a new instance of this class from the XML contained in the file named +filename. Calls #load_from_xml +internally.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 333
+def load_from_file(filename, options={:mapping=>:_default})
+  xml = REXML::Document.new(File.new(filename))
+  load_from_xml xml.root, :mapping=>options[:mapping]
+end
+
+ +
+ + + + +
+ + +
+ +
+ load_from_xml(xml, options={:mapping=>:_default}) + + click to toggle source + +
+ + +
+ +

Create a new instance of this class from the XML contained in xml (a REXML::Element).

+ +

Allocates a new object, then calls fill_from_xml(xml) on it.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 343
+def load_from_xml(xml, options={:mapping=>:_default})
+  raise(MappingError, "undefined mapping: #{options[:mapping].inspect}")            unless xml_mapping_nodes_hash.has_key?(options[:mapping])
+  # create the new object. It is recommended that the class
+  # have a no-argument initializer, so try new first. If that
+  # doesn't work, try allocate, which bypasses the initializer.
+  begin
+    obj = self.new
+    #TODO: this will normally invoke our base XML::Mapping#initialize, which calls
+    #  obj.initialize_xml_mapping, which is called below again (with the correct :mapping parameter).
+    #  obj.initialize_xml_mapping calls obj_initializing on all nodes.
+    #  So obj_initializing may be called on the nodes twice for this initialization.
+    #  Maybe document this for node writers?
+  rescue ArgumentError # TODO: this may hide real errors.
+                       #   how to statically check whether
+                       #   self self.new accepts an empty
+                       #   argument list?
+    obj = self.allocate
+  end
+  obj.initialize_xml_mapping :mapping=>options[:mapping]
+  obj.fill_from_xml xml, :mapping=>options[:mapping]
+  obj
+end
+
+ +
+ + + + +
+ + +
+ +
+ mapping_output_formatter(formatter=nil) + + click to toggle source + +
+ + +
+ +

the formatter to be used for output formatting when writing xml to +character streams (Files/IOs). Combined getter/setter. Defaults to simple +(compact/no-whitespace) formatting, may be overridden on a per-call base +via options

+ + + + +
+
# File lib/xml/mapping/base.rb, line 443
+def mapping_output_formatter(formatter=nil)
+  # TODO make it per-mapping
+  if formatter
+    @mapping_output_formatter = formatter
+  end
+  @mapping_output_formatter ||= REXML::Formatters::Default.new
+end
+
+ +
+ + + + +
+ + +
+ +
+ root_element_name(name=nil, options={:mapping=>@default_mapping}) + + click to toggle source + +
+ + +
+ +

The “root element name” of this class (combined getter/setter method).

+ +

The root element name is the name of the root element of the XML tree returned by <this +class>.#save_to_xml (or, more specifically, <this +class>.#pre_save). By default, this method returns the default_root_element_name; +you may call this method with an argument to set the root element name to +something other than the default. The option argument :mapping specifies +the mapping the root element is/will be defined in, it defaults to the +current default mapping (:_default initially, or the value set with the +latest call to #use_mapping)

+ + + + +
+
# File lib/xml/mapping/base.rb, line 418
+def root_element_name(name=nil, options={:mapping=>@default_mapping})
+  if Hash===name    # ugly...
+    options=name; name=nil
+  end
+  @root_element_names ||= {}
+  if name
+    Classes_by_rootelt_names.remove_class root_element_name, options[:mapping], self
+    @root_element_names[options[:mapping]] = name
+    Classes_by_rootelt_names.create_classes_for(name, options[:mapping]) << self
+  end
+  @root_element_names[options[:mapping]] || default_root_element_name
+end
+
+ +
+ + + + +
+ + +
+ +
+ use_mapping(mapping) + + click to toggle source + +
+ + +
+ +

Make mapping the mapping to be used by default in future node +declarations in this class. The default can be overwritten on a per-node +basis by passing a :mapping option parameter to the node factory method

+ +

The initial default mapping in a mapping class is :_default

+ + + + +
+
# File lib/xml/mapping/base.rb, line 291
+def use_mapping mapping
+  @default_mapping = mapping
+  xml_mapping_nodes_hash[mapping] ||= []  # create empty mapping node list if
+                                          # there wasn't one before so future calls
+                                          # to load/save_xml etc. w/ this mapping don't raise
+end
+
+ +
+ + + + +
+ + +
+ +
+ xml_mapping_nodes(options={:mapping=>nil,:create=>true}) + + click to toggle source + +
+ + +
+ +

array of all nodes defined in this class, in the order of their definition. +Option :create specifies whether or not an empty array should be created +and returned if there was none before (if not, an exception is raised). +:mapping specifies the mapping the returned nodes must have been defined +in; nil means return all nodes regardless of their mapping

+ + + + +
+
# File lib/xml/mapping/base.rb, line 374
+def xml_mapping_nodes(options={:mapping=>nil,:create=>true})
+  unless options[:mapping]
+    return xml_mapping_nodes_hash.values.inject([]){|a1,a2|a1+a2}
+  end
+  options[:create] = true if options[:create].nil?
+  if options[:create]
+    xml_mapping_nodes_hash[options[:mapping]] ||= []
+  else
+    xml_mapping_nodes_hash[options[:mapping]] ||
+      raise(MappingError, "undefined mapping: #{options[:mapping].inspect}")
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/Classes_by_rootelt_names.html b/XML/Mapping/Classes_by_rootelt_names.html new file mode 100644 index 0000000..4a3c7ae --- /dev/null +++ b/XML/Mapping/Classes_by_rootelt_names.html @@ -0,0 +1,252 @@ + + + + + + +module XML::Mapping::Classes_by_rootelt_names - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::Mapping::Classes_by_rootelt_names +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ classes_for(rootelt_name, mapping) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 95
+def classes_for rootelt_name, mapping
+  (self[rootelt_name] || {})[mapping] || []
+end
+
+ +
+ + + + +
+ + +
+ +
+ create_classes_for(rootelt_name, mapping) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 92
+def create_classes_for rootelt_name, mapping
+  (self[rootelt_name] ||= {})[mapping] ||= []
+end
+
+ +
+ + + + +
+ + +
+ +
+ ensure_exists(rootelt_name, mapping, clazz) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 101
+def ensure_exists rootelt_name, mapping, clazz
+  clazzes = create_classes_for(rootelt_name, mapping)
+  clazzes << clazz unless clazzes.include? clazz
+end
+
+ +
+ + + + +
+ + +
+ +
+ remove_class(rootelt_name, mapping, clazz) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 98
+def remove_class rootelt_name, mapping, clazz
+  classes_for(rootelt_name, mapping).delete clazz
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/HashNode.html b/XML/Mapping/HashNode.html new file mode 100644 index 0000000..8942086 --- /dev/null +++ b/XML/Mapping/HashNode.html @@ -0,0 +1,206 @@ + + + + + + +class XML::Mapping::HashNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::HashNode +

+ +
+ +

Node factory function synopsis:

+ +
hash_node :_attrname_, _per_hashelement_path_, _key_path_
+                  [, :default_value=>_obj_]
+                  [, :optional=>true]
+                  [, :class=>_c_]
+                  [, :marshaller=>_proc_]
+                  [, :unmarshaller=>_proc_]
+                  [, :mapping=>_m_]
+                  [, :sub_mapping=>_sm_]
+
  • +

    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.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

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

+ + +
+ Calls superclass method + XML::Mapping::SubObjectBaseNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 335
+def initialize(*args)
+  path1,path2,path3,*args = super(*args)
+  base_path,per_hashelement_path,key_path = if path3
+                                              [path1,path2,path3]
+                                            else
+                                              ["",path1,path2]
+                                            end
+  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)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/Node.html b/XML/Mapping/Node.html new file mode 100644 index 0000000..ca2310c --- /dev/null +++ b/XML/Mapping/Node.html @@ -0,0 +1,548 @@ + + + + + + +class XML::Mapping::Node - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::Node +

+ +
+ +

Abstract base class for all node types. As mentioned in the documentation +for XML::Mapping, node types must be +registered using XML::Mapping.add_node_class, +and a corresponding “node factory method” (e.g. “text_node”) will then be +added as a class method to your mapping classes. The node factory method is +called from the body of the mapping classes as demonstrated in the +examples. It creates an instance of its corresponding node type (the list +of parameters to the node factory method, preceded by the owning mapping +class, will be passed to the constructor of the node type) and adds it to +its owning mapping class, so there is one node object per node definition +per mapping class. That node object will handle all XML marshalling/unmarshalling for this node, for +all instances of the mapping class. For this purpose, the marshalling and +unmarshalling methods of a mapping class instance (fill_into_xml and +fill_from_xml, respectively) will call ::obj_to_xml resp. ::xml_to_obj on all nodes of the +mapping class, in the order of their definition, passing the REXML element the data is to be marshalled +to/unmarshalled from as well as the object the data is to be read +from/filled into.

+ +

Node types that map some XML data to a single attribute of their mapping +class (that should be most of them) shouldn't be directly derived from +this class, but rather from SingleAttributeNode.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ default_obj_to_xml(obj,xml) + +
+ + +
+ + + + + + +
+ + + + +
+ Alias for: obj_to_xml +
+ +
+ + +
+ +
+ default_xml_to_obj(obj,xml) + +
+ + +
+ + + + + + +
+ + + + +
+ Alias for: xml_to_obj +
+ +
+ + +
+ +
+ new(owner,*args) + + click to toggle source + +
+ + +
+ +

Intializer, to be called from descendant classes. owner is the +mapping class this node is being defined in. It'll be stored in +_@owner_. @options will be set to a (possibly empty) hash containing the +option arguments passed to initialize. Options :mapping, :reader +and :writer will be handled, subclasses may handle additional options. See +the section on defining nodes in the README for details.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 550
+def initialize(owner,*args)
+  @owner = owner
+  if Hash===args[-1]
+    @options = args[-1]
+    args = args[0..-2]
+  else
+    @options={}
+  end
+  @mapping = @options[:mapping] || owner.default_mapping
+  owner.xml_mapping_nodes(:mapping=>@mapping) << self
+  XML::Mapping::Classes_by_rootelt_names.ensure_exists owner.root_element_name, @mapping, owner
+  if @options[:reader]
+    # override xml_to_obj in this instance with invocation of
+    # @options[:reader]
+    class << self
+      alias_method :default_xml_to_obj, :xml_to_obj
+      def xml_to_obj(obj,xml)
+        begin
+          @options[:reader].call(obj,xml,self.method(:default_xml_to_obj))
+        rescue ArgumentError  # thrown if @options[:reader] is a lambda (i.e. no Proc) with !=3 args (e.g. proc{...} in ruby1.8)
+          @options[:reader].call(obj,xml)
+        end
+      end
+    end
+  end
+  if @options[:writer]
+    # override obj_to_xml in this instance with invocation of
+    # @options[:writer]
+    class << self
+      alias_method :default_obj_to_xml, :obj_to_xml
+      def obj_to_xml(obj,xml)
+        begin
+          @options[:writer].call(obj,xml,self.method(:default_obj_to_xml))
+        rescue ArgumentError # thrown if (see above)
+          @options[:writer].call(obj,xml)
+        end
+      end
+    end
+  end
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+ obj_to_xml(obj,xml) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 580
+def obj_to_xml(obj,xml)
+  begin
+    @options[:writer].call(obj,xml,self.method(:default_obj_to_xml))
+  rescue ArgumentError # thrown if (see above)
+    @options[:writer].call(obj,xml)
+  end
+end
+
+ +
+ + +
+ Also aliased as: default_obj_to_xml +
+ + + +
+ + +
+ +
+ xml_to_obj(obj,xml) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/mapping/base.rb, line 566
+def xml_to_obj(obj,xml)
+  begin
+    @options[:reader].call(obj,xml,self.method(:default_xml_to_obj))
+  rescue ArgumentError  # thrown if @options[:reader] is a lambda (i.e. no Proc) with !=3 args (e.g. proc{...} in ruby1.8)
+    @options[:reader].call(obj,xml)
+  end
+end
+
+ +
+ + +
+ Also aliased as: default_xml_to_obj +
+ + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ is_present_in?(obj) + + click to toggle source + +
+ + +
+ +

tell whether this node's data is present in obj (when this +method is called, obj will be an instance of the mapping class +this node was defined in). This method is currently used only by ChoiceNode when writing data back to XML. See XML::Mapping::ChoiceNode#obj_to_xml.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 629
+def is_present_in? obj
+  true
+end
+
+ +
+ + + + +
+ + +
+ +
+ obj_initializing(obj,mapping) + + click to toggle source + +
+ + +
+ +

Called when a new instance of the mapping class this node belongs to is +being initialized. obj is the instance. mapping is the +mapping the initialization is happening with, if any: If the instance is +being initialized as part of e.g. Class.load_from_file(name, +:mapping=>:some_mapping or any other call that specifies a +mapping, that mapping will be passed to this method. If the instance is +being initialized normally with Class.new, mapping is +nil here.

+ +

You may set up initial values for the attributes this node is responsible +for here. Default implementation is empty.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 622
+def obj_initializing(obj,mapping)
+end
+
+ +
+ + + + +
+ + +
+ +
+ obj_to_xml(obj,xml) + + click to toggle source + +
+ + +
+ +

This is called by the XML unmarshalling +machinery when the state of an instance of this node's @owner is to be +stored into an XML tree. obj is the +instance, xml is the tree (a REXML::Element). The node must extract +“its” data from obj and store it to the corresponding parts +(sub-elements, attributes etc.) of xml (using XML::XXPath or any other means).

+ + + + +
+
# File lib/xml/mapping/base.rb, line 607
+def obj_to_xml(obj,xml)
+  raise "abstract method called"
+end
+
+ +
+ + + + +
+ + +
+ +
+ xml_to_obj(obj,xml) + + click to toggle source + +
+ + +
+ +

This is called by the XML unmarshalling +machinery when the state of an instance of this node's @owner is to be +read from an XML tree. obj is the +instance, xml is the tree (a REXML::Element). The node must read +“its” data from xml (using XML::XXPath or any other means) and store it to +the corresponding parts (attributes etc.) of obj's state.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 597
+def xml_to_obj(obj,xml)
+  raise "abstract method called"
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/NumericNode.html b/XML/Mapping/NumericNode.html new file mode 100644 index 0000000..866a343 --- /dev/null +++ b/XML/Mapping/NumericNode.html @@ -0,0 +1,171 @@ + + + + + + +class XML::Mapping::NumericNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::NumericNode +

+ +
+ +

Node factory function synopsis:

+ +
numeric_node :_attrname_, _path_ [, :default_value=>_obj_]
+                                 [, :optional=>true]
+                                 [, :mapping=>_m_]
+ +

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.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ + + + +
+ Calls superclass method + XML::Mapping::SingleAttributeNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 46
+def initialize(*args)
+  path,*args = super(*args)
+  @path = XML::XXPath.new(path)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/ObjectNode.html b/XML/Mapping/ObjectNode.html new file mode 100644 index 0000000..eee925a --- /dev/null +++ b/XML/Mapping/ObjectNode.html @@ -0,0 +1,183 @@ + + + + + + +class XML::Mapping::ObjectNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::ObjectNode +

+ +
+ +

Node factory function synopsis:

+ +
object_node :_attrname_, _path_ [, :default_value=>_obj_]
+                                [, :optional=>true]
+                                [, :class=>_c_]
+                                [, :marshaller=>_proc_]
+                                [, :unmarshaller=>_proc_]
+                                [, :mapping=>_m_]
+                                [, :sub_mapping=>_sm_]
+ +

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.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

Initializer. path (a string denoting an XPath expression) is the +location of the subtree.

+ + +
+ Calls superclass method + XML::Mapping::SubObjectBaseNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 167
+def initialize(*args)
+  path,*args = super(*args)
+  @path = XML::XXPath.new(path)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/SingleAttributeNode.html b/XML/Mapping/SingleAttributeNode.html new file mode 100644 index 0000000..a180476 --- /dev/null +++ b/XML/Mapping/SingleAttributeNode.html @@ -0,0 +1,400 @@ + + + + + + +class XML::Mapping::SingleAttributeNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::SingleAttributeNode +

+ +
+ +

Base class for node types that map some XML +data to a single attribute of their mapping class.

+ +

All node types that come with xml-mapping except one (ChoiceNode) inherit +from SingleAttributeNode.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

Initializer. owner is the owning mapping class (gets passed to the +superclass initializer and therefore put into @owner). The second parameter +(and hence the first parameter to the node factory method), +attrname, is a symbol that names the mapping class attribute this +node should map to. It gets stored into @attrname, and the attribute (an +r/w attribute of name attrname) is added to the mapping class (using +attr_accessor).

+ +

In the initializer, two option arguments – :optional and :default_value – +are processed in SingleAttributeNode:

+ +

Supplying :default_value=>obj makes obj the _default +value_ for this attribute. When unmarshalling (loading) an object from an +XML source, the attribute will be set to this +value if nothing was provided in the XML; when +marshalling (saving), the attribute won't be saved if it is set to the +default value.

+ +

Providing just :optional=>true is equivalent to providing +:default_value=>nil.

+ + +
+ Calls superclass method + XML::Mapping::Node.new +
+ + + +
+
# File lib/xml/mapping/base.rb, line 662
+def initialize(*args)
+  @attrname,*args = super(*args)
+  @owner.add_accessor @attrname
+  if @options[:optional] and not(@options.has_key?(:default_value))
+    @options[:default_value] = nil
+  end
+  initialize_impl(*args)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ default_when_xpath_err() { || ... } + + click to toggle source + +
+ + +
+ +

utility method to be used by implementations of extract_attr_value. +Calls the supplied block, catching XML::XXPathError and mapping it to NoAttrValueSet. This is +for the common case that an implementation considers an attribute value not +to be present in the XML if some specific +sub-path does not exist.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 752
+def default_when_xpath_err # :yields:
+  begin
+    yield
+  rescue XML::XXPathError => err
+    raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})"
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+ extract_attr_value(xml) + + click to toggle source + +
+ + +
+ +

(to be overridden by subclasses) Extract and return the value of the +attribute this node is responsible for (@attrname) from xml. If +the implementation decides that the attribute value is “unset” in +xml, it should raise NoAttrValueSet in order +to initiate proper handling of possibly supplied :optional and +:default_value options (you may use default_when_xpath_err +for this purpose).

+ + + + +
+
# File lib/xml/mapping/base.rb, line 713
+def extract_attr_value(xml)
+  raise "abstract method called"
+end
+
+ +
+ + + + +
+ + +
+ +
+ initialize_impl(*args) + + click to toggle source + +
+ + +
+ +

this method was retained for compatibility with xml-mapping 0.8.

+ +

It used to be the initializer to be implemented by subclasses. The +arguments (args) are those still unprocessed by SingleAttributeNode's +initializer.

+ +

In xml-mapping 0.9 and up, you should just override initialize() and call +super.initialize. The returned array is the same args array.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 679
+def initialize_impl(*args)
+end
+
+ +
+ + + + +
+ + +
+ +
+ is_present_in?(obj) + + click to toggle source + +
+ + +
+ +

(overridden) returns true if and only if the value of this node's +attribute in obj is non-nil.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 761
+def is_present_in? obj
+  nil != obj.send(:"#{@attrname}")
+end
+
+ +
+ + + + +
+ + +
+ +
+ set_attr_value(xml, value) + + click to toggle source + +
+ + +
+ +

(to be overridden by subclasses) Write value, which is the current +value of the attribute this node is responsible for (@attrname), into (the +correct sub-nodes, attributes, whatever) of xml.

+ + + + +
+
# File lib/xml/mapping/base.rb, line 734
+def set_attr_value(xml, value)
+  raise "abstract method called"
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/SingleAttributeNode/NoAttrValueSet.html b/XML/Mapping/SingleAttributeNode/NoAttrValueSet.html new file mode 100644 index 0000000..455ccc7 --- /dev/null +++ b/XML/Mapping/SingleAttributeNode/NoAttrValueSet.html @@ -0,0 +1,108 @@ + + + + + + +class XML::Mapping::SingleAttributeNode::NoAttrValueSet - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::SingleAttributeNode::NoAttrValueSet +

+ +
+ +

Exception that may be used by implementations of extract_attr_value to +announce that the attribute value is not set in the XML and, consequently, the default value +should be set in the object being created, or an Exception be raised if no +default value was specified.

+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/Mapping/SubObjectBaseNode.html b/XML/Mapping/SubObjectBaseNode.html new file mode 100644 index 0000000..1dd06e2 --- /dev/null +++ b/XML/Mapping/SubObjectBaseNode.html @@ -0,0 +1,224 @@ + + + + + + +class XML::Mapping::SubObjectBaseNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::SubObjectBaseNode +

+ +
+ +

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

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ +

processes the keyword arguments :class, :marshaller, and :unmarshaller +(args is ignored). When this initiaizer returns, @marshaller and +@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.

+ + +
+ Calls superclass method + XML::Mapping::SingleAttributeNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 101
+def initialize(*args)
+  args = super(*args)
+
+  @sub_mapping = @options[:sub_mapping] || @mapping
+  @marshaller, @unmarshaller = @options[:marshaller], @options[:unmarshaller]
+
+  if @options[:class]
+    unless @marshaller
+      @marshaller = proc {|xml,value|
+        value.fill_into_xml xml, :mapping=>@sub_mapping
+        if xml.unspecified?
+          xml.name = value.class.root_element_name :mapping=>@sub_mapping
+          xml.unspecified = false
+        end
+      }
+    end
+    unless @unmarshaller
+      @unmarshaller = proc {|xml|
+        @options[:class].load_from_xml xml, :mapping=>@sub_mapping
+      }
+    end
+  end
+
+  unless @marshaller
+    @marshaller = proc {|xml,value|
+      value.fill_into_xml xml, :mapping=>@sub_mapping
+      if xml.unspecified?
+        xml.name = value.class.root_element_name :mapping=>@sub_mapping
+        xml.unspecified = false
+      end
+    }
+  end
+  unless @unmarshaller
+    @unmarshaller = proc {|xml|
+      XML::Mapping.load_object_from_xml xml, :mapping=>@sub_mapping
+    }
+  end
+
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/Mapping/TextNode.html b/XML/Mapping/TextNode.html new file mode 100644 index 0000000..72ee97b --- /dev/null +++ b/XML/Mapping/TextNode.html @@ -0,0 +1,176 @@ + + + + + + +class XML::Mapping::TextNode - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::Mapping::TextNode +

+ +
+ +

Node factory function synopsis:

+ +
text_node :_attrname_, _path_ [, :default_value=>_obj_]
+                              [, :optional=>true]
+                              [, :mapping=>_m_]
+ +

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. +path (an XPath expression) locates the XML node, attrname (a symbol) names the +attribute. :default_value is the default value, +:optional=>true is equivalent to :default_value=>nil (see superclass +documentation for details). m is the mapping; it +defaults to the current default mapping

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(*args) + + click to toggle source + +
+ + +
+ + + + +
+ Calls superclass method + XML::Mapping::SingleAttributeNode.new +
+ + + +
+
# File lib/xml/mapping/standard_nodes.rb, line 23
+def initialize(*args)
+  path,*args = super(*args)
+  @path = XML::XXPath.new(path)
+  args
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/MappingError.html b/XML/MappingError.html new file mode 100644 index 0000000..bfb4ead --- /dev/null +++ b/XML/MappingError.html @@ -0,0 +1,102 @@ + + + + + + +class XML::MappingError - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::MappingError +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath.html b/XML/XXPath.html new file mode 100644 index 0000000..7ac1137 --- /dev/null +++ b/XML/XXPath.html @@ -0,0 +1,376 @@ + + + + + + +class XML::XXPath - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::XXPath +

+ +
+ +

Instances of this class hold (in a pre-compiled form) an XPath pattern. You +call instance methods like each, first, +all, create_new on instances of this class to +apply the pattern to REXML elements.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(xpathstr) + + click to toggle source + +
+ + +
+ +

create and compile a new XPath. xpathstr is the string +representation (XPath pattern) of the path

+ + + + +
+
# File lib/xml/xxpath.rb, line 21
+def initialize(xpathstr)
+  @xpathstr = xpathstr  # for error messages
+
+  # TODO: write a real XPath parser sometime
+
+  xpathstr='/'+xpathstr if xpathstr[0] != ?/
+
+  @creator_procs = [ proc{|node,create_new| node} ]
+  @reader_proc = proc {|nodes| nodes}
+  
+  part=nil; part_expected=true
+  xpathstr.split(/(\/+)/)[1..-1].reverse.each do |x|
+    if part_expected
+      part=x
+      part_expected = false
+      next
+    end
+    part_expected = true
+    axis = case x
+           when '/'
+             :child
+           when '//'
+             :descendant
+           else
+             raise XXPathError, "XPath (#{xpathstr}): unknown axis: #{x}"
+           end
+    axis=:self if axis==:child and (part[0]==?. or part=~/^self::/)  # yuck
+
+    step = Step.compile(axis,part)
+    @creator_procs << step.creator(@creator_procs[-1])
+    @reader_proc = step.reader(@reader_proc, @creator_procs[-1])
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ all(node,options={}) + + click to toggle source + +
+ + +
+ +

Return an Enumerable with all sub-nodes of node that match this +XPath. Returns an empty Enumerable if no match was found.

+ +

If :ensure_created=>true is provided, all() ensures that a match exists +in node, creating one (and returning it as the sole element of the +returned enumerable) if none existed before.

+ + + + +
+
# File lib/xml/xxpath.rb, line 88
+def all(node,options={})
+  raise "options not a hash" unless Hash===options
+  if options[:create_new]
+    return [ @creator_procs[-1].call(node,true) ]
+  else
+    last_nodes,rest_creator = catch(:not_found) do
+      return @reader_proc.call([node])
+    end
+    if options[:ensure_created]
+      [ rest_creator.call(last_nodes[0],false) ]
+    else
+      []
+    end
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+ create_new(base_node) + + click to toggle source + +
+ + +
+ +

create a completely new match of this XPath in base_node. +“Completely new” means that a new node will be created for each path +element, even if a matching node already existed in base_node.

+ +

path.create_new(node) is equivalent to +path.first(node,:create_new=>true).

+ + + + +
+
# File lib/xml/xxpath.rb, line 111
+def create_new(base_node)
+  first(base_node,:create_new=>true)
+end
+
+ +
+ + + + +
+ + +
+ +
+ each(node,options={},&block) + + click to toggle source + +
+ + +
+ +

loop over all sub-nodes of node that match this XPath.

+ + + + +
+
# File lib/xml/xxpath.rb, line 57
+def each(node,options={},&block)
+  all(node,options).each(&block)
+end
+
+ +
+ + + + +
+ + +
+ +
+ first(node,options={}) + + click to toggle source + +
+ + +
+ +

the first sub-node of node that matches this XPath. If nothing +matches, raise XXPathError unless +:allow_nil=>true was provided.

+ +

If :ensure_created=>true is provided, first() ensures that a match +exists in node, creating one if none existed before.

+ +

path.first(node,:create_new=>true) is equivalent to +path.create_new(node).

+ + + + +
+
# File lib/xml/xxpath.rb, line 69
+def first(node,options={})
+  a=all(node,options)
+  if a.empty?
+    if options[:allow_nil]
+      nil
+    else
+      raise XXPathError, "path not found: #{@xpathstr}"
+    end
+  else
+    a[0]
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/XXPath/Accessors.html b/XML/XXPath/Accessors.html new file mode 100644 index 0000000..7a127e6 --- /dev/null +++ b/XML/XXPath/Accessors.html @@ -0,0 +1,95 @@ + + + + + + +module XML::XXPath::Accessors - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::Accessors +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath/Accessors/Attribute.html b/XML/XXPath/Accessors/Attribute.html new file mode 100644 index 0000000..279a30d --- /dev/null +++ b/XML/XXPath/Accessors/Attribute.html @@ -0,0 +1,371 @@ + + + + + + +class XML::XXPath::Accessors::Attribute - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::XXPath::Accessors::Attribute +

+ +
+ +

attribute node, more or less call-compatible with REXML's Element. +REXML's Attribute class doesn't +provide this…

+ +

The all/first calls return instances of this class if they matched an +attribute node.

+ +
+ + + + +
+ + + + + + + +
+
+

Attributes

+
+ + +
+
+ name[RW] +
+ +
+ + + +
+
+ +
+
+ parent[R] +
+ +
+ + + +
+
+ +
+ + + +
+
+

Public Class Methods

+
+ + +
+ +
+ new(parent,name) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 74
+def initialize(parent,name)
+  @parent,@name = parent,name
+end
+
+ +
+ + + + +
+ + +
+ +
+ new(parent,name,create) + + click to toggle source + +
+ + +
+ + + + +
+ Calls superclass method + +
+ + + +
+
# File lib/xml/rexml_ext.rb, line 78
+def self.new(parent,name,create)
+  if parent.attributes[name]
+    super(parent,name)
+  else
+    if create
+      parent.attributes[name] = "[unset]"
+      super(parent,name)
+    else
+      nil
+    end
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ ==(other) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 100
+def ==(other)
+  other.kind_of?(Attribute) and other.parent==parent and other.name==name
+end
+
+ +
+ + + + +
+ + +
+ +
+ text() + + click to toggle source + +
+ + +
+ +

the value of the attribute.

+ + + + +
+
# File lib/xml/rexml_ext.rb, line 92
+def text
+  parent.attributes[@name]
+end
+
+ +
+ + + + +
+ + +
+ +
+ text=(x) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 96
+def text=(x)
+  parent.attributes[@name] = x
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/XXPath/Accessors/REXML.html b/XML/XXPath/Accessors/REXML.html new file mode 100644 index 0000000..804d2c9 --- /dev/null +++ b/XML/XXPath/Accessors/REXML.html @@ -0,0 +1,95 @@ + + + + + + +module XML::XXPath::Accessors::REXML - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::Accessors::REXML +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath/Accessors/UnspecifiednessSupport.html b/XML/XXPath/Accessors/UnspecifiednessSupport.html new file mode 100644 index 0000000..759d3f8 --- /dev/null +++ b/XML/XXPath/Accessors/UnspecifiednessSupport.html @@ -0,0 +1,265 @@ + + + + + + +module XML::XXPath::Accessors::UnspecifiednessSupport - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::Accessors::UnspecifiednessSupport +

+ +
+ +

we need a boolean “unspecified?” attribute for XML nodes – paths like “*” oder (somewhen) “foo|bar” +create “unspecified” nodes that the user must then “specify” by setting +their text etc. (or manually setting unspecified=false)

+ +

This is mixed into the REXML::Element and XML::XXPath::Accessors::Attribute classes.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Class Methods

+
+ + +
+ +
+ append_features(base) + + click to toggle source + +
+ + +
+ + + + +
+ Calls superclass method + +
+ + + +
+
# File lib/xml/rexml_ext.rb, line 28
+def self.append_features(base)
+  return if base.included_modules.include? self # avoid aliasing methods more than once
+                                                # (would lead to infinite recursion)
+  super
+  base.module_eval <<-EOS
+    alias_method :_text_orig, :text
+    alias_method :_textis_orig, :text=
+    def text
+      # we're suffering from the "fragile base class"
+      # phenomenon here -- we don't know whether the
+      # implementation of the class we get mixed into always
+      # calls text (instead of just accessing @text or so)
+      if unspecified?
+        "[UNSPECIFIED]"
+      else
+        _text_orig
+      end
+    end
+    def text=(x)
+      _textis_orig(x)
+      self.unspecified=false
+    end
+
+    alias_method :_nameis_orig, :name=
+    def name=(x)
+      _nameis_orig(x)
+      self.unspecified=false
+    end
+  EOS
+end
+
+ +
+ + + + +
+ + +
+ +
+
+

Public Instance Methods

+
+ + +
+ +
+ unspecified=(x) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 24
+def unspecified=(x)
+  @xml_xpath_unspecified = x
+end
+
+ +
+ + + + +
+ + +
+ +
+ unspecified?() + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/rexml_ext.rb, line 20
+def unspecified?
+  @xml_xpath_unspecified ||= false
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/XML/XXPath/XML.html b/XML/XXPath/XML.html new file mode 100644 index 0000000..50fba55 --- /dev/null +++ b/XML/XXPath/XML.html @@ -0,0 +1,95 @@ + + + + + + +module XML::XXPath::XML - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::XML +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath/XML/XXPath.html b/XML/XXPath/XML/XXPath.html new file mode 100644 index 0000000..0cbb7b9 --- /dev/null +++ b/XML/XXPath/XML/XXPath.html @@ -0,0 +1,95 @@ + + + + + + +module XML::XXPath::XML::XXPath - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::XML::XXPath +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath/XML/XXPath/REXML.html b/XML/XXPath/XML/XXPath/REXML.html new file mode 100644 index 0000000..a3009c7 --- /dev/null +++ b/XML/XXPath/XML/XXPath/REXML.html @@ -0,0 +1,95 @@ + + + + + + +module XML::XXPath::XML::XXPath::REXML - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPath::XML::XXPath::REXML +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPath/XML/XXPath/REXML/Text.html b/XML/XXPath/XML/XXPath/REXML/Text.html new file mode 100644 index 0000000..db8ca49 --- /dev/null +++ b/XML/XXPath/XML/XXPath/REXML/Text.html @@ -0,0 +1,102 @@ + + + + + + +class XML::XXPath::XML::XXPath::REXML::Text - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::XXPath::XML::XXPath::REXML::Text +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPathError.html b/XML/XXPathError.html new file mode 100644 index 0000000..e97e62d --- /dev/null +++ b/XML/XXPathError.html @@ -0,0 +1,102 @@ + + + + + + +class XML::XXPathError - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ class XML::XXPathError +

+ +
+ +
+ + + + +
+ + + + + + + + + +
+
+ + + + diff --git a/XML/XXPathMethods.html b/XML/XXPathMethods.html new file mode 100644 index 0000000..27bc690 --- /dev/null +++ b/XML/XXPathMethods.html @@ -0,0 +1,321 @@ + + + + + + +module XML::XXPathMethods - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+

+ module XML::XXPathMethods +

+ +
+ +

set of convenience wrappers around XML::XPath's instance methods, for +people who frequently use XML::XPath directly. This module is included into +the REXML node classes and adds methods to them +that enable you to write a call like

+ +
path.first(xml_element)
+
+ +

as the more pleasant-looking variant

+ +
xml_element.first_xpath(path)
+
+ +

with the added bonus that path may not only be an XML::XXPath instance, but also just a String containing the XPath expression.

+ +

Please note that the names of all the added methods are suffixed with +“_xpath” to avoid name clashes with REXML +methods. Please note also that this was changed recently, so older versions +of xml-mapping (version < 0.9) used method names without _xpath appended +and thus would be incompatible with this one here.

+ +

As a special convenience, if you're using an older version of REXML that doesn't have the new methods yet, +methods without _xpath in their names will also (additionally) be added to +the REXML classes. This will enable code that +relied on the old names to keep on working as long as REXML isn't updated, at which point that code +will fail and must be changed to used the methods suffixed with _xpath.

+ +
+ + + + +
+ + + + + + + + + +
+
+

Public Instance Methods

+
+ + +
+ +
+ all_xpath(path,options={}) + + click to toggle source + +
+ + +
+ +

see XML::XXPath#all

+ + + + +
+
# File lib/xml/xxpath_methods.rb, line 47
+def all_xpath(path,options={})
+  to_xxpath(path).all self, options
+end
+
+ +
+ + + + +
+ + +
+ +
+ create_new_xpath(path) + + click to toggle source + +
+ + +
+ +

see XML::XXPath#create_new

+ + + + +
+
# File lib/xml/xxpath_methods.rb, line 52
+def create_new_xpath(path)
+  to_xxpath(path).create_new self
+end
+
+ +
+ + + + +
+ + +
+ +
+ each_xpath(path,options={},&block) + + click to toggle source + +
+ + +
+ +

see XML::XXPath#each

+ + + + +
+
# File lib/xml/xxpath_methods.rb, line 37
+def each_xpath(path,options={},&block)
+  to_xxpath(path).each self, options, &block
+end
+
+ +
+ + + + +
+ + +
+ +
+ first_xpath(path,options={}) + + click to toggle source + +
+ + +
+ +

see XML::XXPath#first

+ + + + +
+
# File lib/xml/xxpath_methods.rb, line 42
+def first_xpath(path,options={})
+  to_xxpath(path).first self, options
+end
+
+ +
+ + + + +
+ + +
+ +
+ to_xxpath(path) + + click to toggle source + +
+ + +
+ + + + + + +
+
# File lib/xml/xxpath_methods.rb, line 75
+def to_xxpath(path)
+  if String===path
+    XXPath.new path
+  else
+    path
+  end
+end
+
+ +
+ + + + +
+ + +
+ +
+
+ + + + diff --git a/created.rid b/created.rid new file mode 100644 index 0000000..2187c66 --- /dev/null +++ b/created.rid @@ -0,0 +1,15 @@ +Sun, 01 Mar 2015 15:34:17 +0100 +user_manual.md Sun, 01 Mar 2015 15:34:16 +0100 +README.md Mon, 23 Feb 2015 00:01:13 +0100 +user_manual_xxpath.md Sun, 01 Mar 2015 15:31:06 +0100 +TODO.txt Sun, 01 Mar 2015 11:32:46 +0100 +doc/xpath_impl_notes.txt Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/mapping.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/mapping/base.rb Sun, 01 Mar 2015 11:32:46 +0100 +lib/xml/mapping/core_classes_mapping.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/mapping/standard_nodes.rb Sun, 01 Mar 2015 12:15:22 +0100 +lib/xml/mapping/version.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/rexml_ext.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/xxpath.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/xxpath/steps.rb Mon, 23 Feb 2015 00:01:13 +0100 +lib/xml/xxpath_methods.rb Mon, 23 Feb 2015 00:01:13 +0100 diff --git a/css/fonts.css b/css/fonts.css new file mode 100644 index 0000000..e9e7211 --- /dev/null +++ b/css/fonts.css @@ -0,0 +1,167 @@ +/* + * Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), + * with Reserved Font Name "Source". All Rights Reserved. Source is a + * trademark of Adobe Systems Incorporated in the United States and/or other + * countries. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 400; + src: local("Source Code Pro"), + local("SourceCodePro-Regular"), + url("fonts/SourceCodePro-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Source Code Pro"; + font-style: normal; + font-weight: 700; + src: local("Source Code Pro Bold"), + local("SourceCodePro-Bold"), + url("fonts/SourceCodePro-Bold.ttf") format("truetype"); +} + +/* + * Copyright (c) 2010, Łukasz Dziedzic (dziedzic@typoland.com), + * with Reserved Font Name Lato. + * + * This Font Software is licensed under the SIL Open Font License, Version + * 1.1. + * + * This license is copied below, and is also available with a FAQ at: + * http://scripts.sil.org/OFL + */ + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 300; + src: local("Lato Light"), + local("Lato-Light"), + url("fonts/Lato-Light.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 300; + src: local("Lato Light Italic"), + local("Lato-LightItalic"), + url("fonts/Lato-LightItalic.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Regular"), + local("Lato-Regular"), + url("fonts/Lato-Regular.ttf") format("truetype"); +} + +@font-face { + font-family: "Lato"; + font-style: italic; + font-weight: 700; + src: local("Lato Italic"), + local("Lato-Italic"), + url("fonts/Lato-RegularItalic.ttf") format("truetype"); +} + +/* + * ----------------------------------------------------------- + * SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + * ----------------------------------------------------------- + * + * PREAMBLE + * The goals of the Open Font License (OFL) are to stimulate worldwide + * development of collaborative font projects, to support the font creation + * efforts of academic and linguistic communities, and to provide a free and + * open framework in which fonts may be shared and improved in partnership + * with others. + * + * The OFL allows the licensed fonts to be used, studied, modified and + * redistributed freely as long as they are not sold by themselves. The + * fonts, including any derivative works, can be bundled, embedded, + * redistributed and/or sold with any software provided that any reserved + * names are not used by derivative works. The fonts and derivatives, + * however, cannot be released under any other type of license. The + * requirement for fonts to remain under this license does not apply + * to any document created using the fonts or their derivatives. + * + * DEFINITIONS + * "Font Software" refers to the set of files released by the Copyright + * Holder(s) under this license and clearly marked as such. This may + * include source files, build scripts and documentation. + * + * "Reserved Font Name" refers to any names specified as such after the + * copyright statement(s). + * + * "Original Version" refers to the collection of Font Software components as + * distributed by the Copyright Holder(s). + * + * "Modified Version" refers to any derivative made by adding to, deleting, + * or substituting -- in part or in whole -- any of the components of the + * Original Version, by changing formats or by porting the Font Software to a + * new environment. + * + * "Author" refers to any designer, engineer, programmer, technical + * writer or other person who contributed to the Font Software. + * + * PERMISSION & CONDITIONS + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of the Font Software, to use, study, copy, merge, embed, modify, + * redistribute, and sell modified and unmodified copies of the Font + * Software, subject to the following conditions: + * + * 1) Neither the Font Software nor any of its individual components, + * in Original or Modified Versions, may be sold by itself. + * + * 2) Original or Modified Versions of the Font Software may be bundled, + * redistributed and/or sold with any software, provided that each copy + * contains the above copyright notice and this license. These can be + * included either as stand-alone text files, human-readable headers or + * in the appropriate machine-readable metadata fields within text or + * binary files as long as those fields can be easily viewed by the user. + * + * 3) No Modified Version of the Font Software may use the Reserved Font + * Name(s) unless explicit written permission is granted by the corresponding + * Copyright Holder. This restriction only applies to the primary font name as + * presented to the users. + * + * 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + * Software shall not be used to promote, endorse or advertise any + * Modified Version, except to acknowledge the contribution(s) of the + * Copyright Holder(s) and the Author(s) or with their explicit written + * permission. + * + * 5) The Font Software, modified or unmodified, in part or in whole, + * must be distributed entirely under this license, and must not be + * distributed under any other license. The requirement for fonts to + * remain under this license does not apply to any document created + * using the Font Software. + * + * TERMINATION + * This license becomes null and void if any of the above conditions are + * not met. + * + * DISCLAIMER + * THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + * OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL + * DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM + * OTHER DEALINGS IN THE FONT SOFTWARE. + */ + diff --git a/css/rdoc.css b/css/rdoc.css new file mode 100644 index 0000000..2f4dca7 --- /dev/null +++ b/css/rdoc.css @@ -0,0 +1,590 @@ +/* + * "Darkfish" Rdoc CSS + * $Id: rdoc.css 54 2009-01-27 01:09:48Z deveiant $ + * + * Author: Michael Granger + * + */ + +/* vim: ft=css et sw=2 ts=2 sts=2 */ +/* Base Green is: #6C8C22 */ + +* { padding: 0; margin: 0; } + +body { + background: #fafafa; + font-family: Lato, sans-serif; + font-weight: 300; +} + +h1 span, +h2 span, +h3 span, +h4 span, +h5 span, +h6 span { + position: relative; + + display: none; + padding-left: 1em; + line-height: 0; + vertical-align: baseline; + font-size: 10px; +} + +h1 span { top: -1.3em; } +h2 span { top: -1.2em; } +h3 span { top: -1.0em; } +h4 span { top: -0.8em; } +h5 span { top: -0.5em; } +h6 span { top: -0.5em; } + +h1:hover span, +h2:hover span, +h3:hover span, +h4:hover span, +h5:hover span, +h6:hover span { + display: inline; +} + +:link, +:visited { + color: #6C8C22; + text-decoration: none; +} + +:link:hover, +:visited:hover { + border-bottom: 1px dotted #6C8C22; +} + +code, +pre { + font-family: "Source Code Pro", Monaco, monospace; +} + +/* @group Generic Classes */ + +.initially-hidden { + display: none; +} + +#search-field { + width: 98%; + background: white; + border: none; + height: 1.5em; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + text-align: left; +} +#search-field:focus { + background: #f1edba; +} +#search-field:-moz-placeholder, +#search-field::-webkit-input-placeholder { + font-weight: bold; + color: #666; +} + +.missing-docs { + font-size: 120%; + background: white url(images/wrench_orange.png) no-repeat 4px center; + color: #ccc; + line-height: 2em; + border: 1px solid #d00; + opacity: 1; + padding-left: 20px; + text-indent: 24px; + letter-spacing: 3px; + font-weight: bold; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +.target-section { + border: 2px solid #dcce90; + border-left-width: 8px; + padding: 0 1em; + background: #fff3c2; +} + +/* @end */ + +/* @group Index Page, Standalone file pages */ +.table-of-contents ul { + margin: 1em; + list-style: none; +} + +.table-of-contents ul ul { + margin-top: 0.25em; +} + +.table-of-contents ul :link, +.table-of-contents ul :visited { + font-size: 16px; +} + +.table-of-contents li { + margin-bottom: 0.25em; +} + +.table-of-contents li .toc-toggle { + width: 16px; + height: 16px; + background: url(images/add.png) no-repeat; +} + +.table-of-contents li .toc-toggle.open { + background: url(images/delete.png) no-repeat; +} + +/* @end */ + +/* @group Top-Level Structure */ + +nav { + float: left; + width: 260px; + font-family: Helvetica, sans-serif; + font-size: 14px; +} + +main { + display: block; + margin: 0 2em 5em 260px; + padding-left: 20px; + min-width: 340px; + font-size: 16px; +} + +main h1, +main h2, +main h3, +main h4, +main h5, +main h6 { + font-family: Helvetica, sans-serif; +} + +.table-of-contents main { + margin-left: 2em; +} + +#validator-badges { + clear: both; + margin: 1em 1em 2em; + font-size: smaller; +} + +/* @end */ + +/* @group navigation */ +nav { + margin-bottom: 1em; +} + +nav .nav-section { + margin-top: 2em; + border-top: 2px solid #aaa; + font-size: 90%; + overflow: hidden; +} + +nav h2 { + margin: 0; + padding: 2px 8px 2px 8px; + background-color: #e8e8e8; + color: #555; + font-size: 125%; + text-align: center; +} + +nav h3, +#table-of-contents-navigation { + margin: 0; + padding: 2px 8px 2px 8px; + text-align: right; + background-color: #e8e8e8; + color: #555; +} + +nav ul, +nav dl, +nav p { + padding: 4px 8px 0; + list-style: none; +} + +#project-navigation .nav-section { + margin: 0; + border-top: 0; +} + +#home-section h2 { + text-align: center; +} + +#table-of-contents-navigation { + font-size: 1.2em; + font-weight: bold; + text-align: center; +} + +#search-section { + margin-top: 0; + border-top: 0; +} + +#search-field-wrapper { + border-top: 1px solid #aaa; + border-bottom: 1px solid #aaa; + padding: 3px 8px; + background-color: #e8e8e8; + color: #555; +} + +ul.link-list li { + white-space: nowrap; + line-height: 1.4em; +} + +ul.link-list .type { + font-size: 8px; + text-transform: uppercase; + color: white; + background: #969696; + padding: 2px 4px; + -webkit-border-radius: 5px; +} + +.calls-super { + background: url(images/arrow_up.png) no-repeat right center; +} + +/* @end */ + +/* @group Documentation Section */ +main { + color: #333; +} + +main > h1:first-child, +main > h2:first-child, +main > h3:first-child, +main > h4:first-child, +main > h5:first-child, +main > h6:first-child { + margin-top: 0px; +} + +main sup { + vertical-align: super; + font-size: 0.8em; +} + +/* The heading with the class name */ +main h1[class] { + margin-top: 0; + margin-bottom: 1em; + font-size: 2em; + color: #6C8C22; +} + +main h1 { + margin: 2em 0 0.5em; + font-size: 1.7em; +} + +main h2 { + margin: 2em 0 0.5em; + font-size: 1.5em; +} + +main h3 { + margin: 2em 0 0.5em; + font-size: 1.2em; +} + +main h4 { + margin: 2em 0 0.5em; + font-size: 1.1em; +} + +main h5 { + margin: 2em 0 0.5em; + font-size: 1em; +} + +main h6 { + margin: 2em 0 0.5em; + font-size: 1em; +} + +main p { + margin: 0 0 0.5em; + line-height: 1.4em; +} + +main pre { + margin: 1.2em 0.5em; + padding: 1em; + font-size: 0.8em; +} + +main hr { + margin: 1.5em 1em; + border: 2px solid #ddd; +} + +main blockquote { + margin: 0 2em 1.2em 1.2em; + padding-left: 0.5em; + border-left: 2px solid #ddd; +} + +main ol, +main ul { + margin: 1em 2em; +} + +main li > p { + margin-bottom: 0.5em; +} + +main dl { + margin: 1em 0.5em; +} + +main dt { + margin-bottom: 0.5em; + font-weight: bold; +} + +main dd { + margin: 0 1em 1em 0.5em; +} + +main header h2 { + margin-top: 2em; + border-width: 0; + border-top: 4px solid #bbb; + font-size: 130%; +} + +main header h3 { + margin: 2em 0 1.5em; + border-width: 0; + border-top: 3px solid #bbb; + font-size: 120%; +} + +.documentation-section-title { + position: relative; +} +.documentation-section-title .section-click-top { + position: absolute; + top: 6px; + left: 12px; + font-size: 10px; + color: #9b9877; + visibility: hidden; + padding-left: 0.5px; +} + +.documentation-section-title:hover .section-click-top { + visibility: visible; +} + +.constants-list > dl { + margin: 1em 0 2em; + border: 0; +} + +.constants-list > dl dt { + margin-bottom: 0.75em; + padding-left: 0; + font-family: "Source Code Pro", Monaco, monospace; + font-size: 110%; +} + +.constants-list > dl dt a { + color: inherit; +} + +.constants-list > dl dd { + margin: 0 0 2em 0; + padding: 0; + color: #666; +} + +.documentation-section h2 { + position: relative; +} + +.documentation-section h2 a { + position: absolute; + top: 8px; + right: 10px; + font-size: 12px; + color: #9b9877; + visibility: hidden; +} + +.documentation-section h2:hover a { + visibility: visible; +} + +/* @group Method Details */ + +main .method-source-code { + display: none; +} + +main .method-description .method-calls-super { + color: #333; + font-weight: bold; +} + +main .method-detail { + margin-bottom: 2.5em; + cursor: pointer; +} + +main .method-detail:target { + margin-left: -10px; + border-left: 10px solid #f1edba; +} + +main .method-heading { + position: relative; + font-family: "Source Code Pro", Monaco, monospace; + font-size: 110%; + font-weight: bold; + color: #333; +} +main .method-heading :link, +main .method-heading :visited { + color: inherit; +} +main .method-click-advice { + position: absolute; + top: 2px; + right: 5px; + font-size: 12px; + color: #9b9877; + visibility: hidden; + padding-right: 20px; + line-height: 20px; + background: url(images/zoom.png) no-repeat right top; +} +main .method-heading:hover .method-click-advice { + visibility: visible; +} + +main .method-alias .method-heading { + color: #666; +} + +main .method-description, +main .aliases { + margin-top: 0.75em; + color: #333; +} + +main .aliases { + padding-top: 4px; + font-style: italic; + cursor: default; +} +main .method-description ul { + margin-left: 1.5em; +} + +main #attribute-method-details .method-detail:hover { + background-color: transparent; + cursor: default; +} +main .attribute-access-type { + text-transform: uppercase; + padding: 0 1em; +} +/* @end */ + +/* @end */ + +/* @group Source Code */ + +pre { + margin: 0.5em 0; + border: 1px dashed #999; + padding: 0.5em; + background: #262626; + color: white; + overflow: auto; +} + +.ruby-constant { color: #7fffd4; background: transparent; } +.ruby-keyword { color: #00ffff; background: transparent; } +.ruby-ivar { color: #eedd82; background: transparent; } +.ruby-operator { color: #00ffee; background: transparent; } +.ruby-identifier { color: #ffdead; background: transparent; } +.ruby-node { color: #ffa07a; background: transparent; } +.ruby-comment { color: #dc0000; background: transparent; } +.ruby-regexp { color: #ffa07a; background: transparent; } +.ruby-value { color: #7fffd4; background: transparent; } + +/* @end */ + + +/* @group search results */ +#search-results { + font-family: Lato, sans-serif; + font-weight: 300; +} + +#search-results .search-match { + font-family: Helvetica, sans-serif; + font-weight: normal; +} + +#search-results .search-selected { + background: #e8e8e8; + border-bottom: 1px solid transparent; +} + +#search-results li { + list-style: none; + border-bottom: 1px solid #aaa; + margin-bottom: 0.5em; +} + +#search-results li:last-child { + border-bottom: none; + margin-bottom: 0; +} + +#search-results li p { + padding: 0; + margin: 0.5em; +} + +#search-results .search-namespace { + font-weight: bold; +} + +#search-results li em { + background: yellow; + font-style: normal; +} + +#search-results pre { + margin: 0.5em; + font-family: "Source Code Pro", Monaco, monospace; +} + +/* @end */ + diff --git a/devel/runinteractive b/devel/runinteractive deleted file mode 100755 index 6144c7b..0000000 --- a/devel/runinteractive +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -export RAILS_ENV=test - -if [ -n "$1" ]; then - require_cmdline_lib="require \"$1\"" -fi - -ruby -r irb -e " -$:.unshift \"../lib\" -$:.unshift \"../test\" - -$require_cmdline_lib -IRB.start -" diff --git a/devel/xml_mapping_intinit.rb b/devel/xml_mapping_intinit.rb deleted file mode 100644 index 9ff1728..0000000 --- a/devel/xml_mapping_intinit.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'company' -@xml = REXML::Document.new(File.new("../test/fixtures/company1.xml")) -@c = Company.load_from_xml(@xml.root) - - -# REXML::XPath is missing all()... -def xpathall(path,xml) - r=[] - XPath.each(xml,path){|x|r<//.../ is -compiled into a bunch of nested closures, each of which is responsible -for a specific path element and calls the corresponding accessor -function: - -- @creator_procs -- an array of "creator" - functions. @creator_procs[i] gets passed a base node (XML - element) and a create_new flag, and it creates the path - //.../ inside the - base node and returns the hindmost element created (i.e. the one - corresponding to ). - -- @reader_proc -- a "reader" function that gets passed an - array of nodes and returns an array of all nodes that matched the - path in any of the supplied nodes, or, if no match was found, throws - :not_found along with the last non-empty set of nodes that was - found, and the element of @creator_procs that could be used - to create the remaining part of the path. - -The +all+ function is then trivially implemented on top of this: - - def all(node,options={}) - raise "options not a hash" unless Hash===options - if options[:create_new] - return [ @creator_procs[-1].call(node,true) ] - else - last_nodes,rest_creator = catch(:not_found) do - return @reader_proc.call([node]) - end - if options[:ensure_created] - [ rest_creator.call(last_nodes[0],false) ] - else - [] - end - end - end - -...and +first+, create_new etc. are even more trivial -frontends to that. - -The implementations of the @creator_procs look like this: - - @creator_procs[0] = - proc{|node,create_new| node} - - @creator_procs[1] = - proc {|node,create_new| - @creator_procs[0].call(Accessors.create_subnode_by_(node,create_new,), - create_new) - } - - @creator_procs[2] = - proc {|node,create_new| - @creator_procs[1].call(Accessors.create_subnode_by_(node,create_new,), - create_new) - } - - ... - - @creator_procs[n] = - proc {|node,create_new| - @creator_procs[n-1].call(Accessors.create_subnode_by_(node,create_new,), - create_new) - } - - ... - @creator_procs[x] = - proc {|node,create_new| - @creator_procs[x-1].call(Accessors.create_subnode_by_(node,create_new,), - create_new) - } - - - -..and the implementation of @reader_proc looks like this: - - @reader_proc = rpx where - - rp0 = proc {|nodes| nodes} - - rp1 = proc {|nodes| - next_nodes = Accessors.subnodes_by_(nodes,) - if (next_nodes == []) - throw :not_found, [nodes,@creator_procs[1]] - else - rp0.call(next_nodes) - end - } - - rp2 = proc {|nodes| - next_nodes = Accessors.subnodes_by_(nodes,) - if (next_nodes == []) - throw :not_found, [nodes,@creator_procs[2]] - else - rp1.call(next_nodes) - end - } - ... - - rpx = proc {|nodes| - next_nodes = Accessors.subnodes_by_(nodes,) - if (next_nodes == []) - throw :not_found, [nodes,@creator_procs[x]] - else - rpx-1.call(next_nodes) - end - } diff --git a/doc/xpath_impl_notes_txt.html b/doc/xpath_impl_notes_txt.html new file mode 100644 index 0000000..830f4ba --- /dev/null +++ b/doc/xpath_impl_notes_txt.html @@ -0,0 +1,207 @@ + + + + + + +xpath_impl_notes - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ +

latest design (12/2004)

+ +

At the lowest level, the “Accessors” sub-module contains reader and creator +functions that correspond to the various types of path elements +(elt_name, @attr_name, elt_name etc.) that xml-xxpath +supports. A reader function gets an array of nodes and the search +parameters corresponding to its path element type (e.g. elt_name, +attr_name, attr_value) and returns an array with all +matching direct sub-nodes of any of the supplied nodes. A creator function +gets one node and the search parameters and returns the created sub-node.

+ +

An XPath expression +<things1>/<things2>/.../<thingsx> is +compiled into a bunch of nested closures, each of which is responsible for +a specific path element and calls the corresponding accessor function:

+
  • +

    @creator_procs – an array of “creator” functions. +@creator_procs[i] gets passed a base node (XML element) and a +create_new flag, and it creates the path +<things[x-i+1]>/<things[x-i+2]>/.../<thingsx> +inside the base node and returns the hindmost element created (i.e. the one +corresponding to <thingsx>).

    +
  • +

    @reader_proc – a “reader” function that gets passed an array +of nodes and returns an array of all nodes that matched the path in any of +the supplied nodes, or, if no match was found, throws :not_found along with +the last non-empty set of nodes that was found, and the element of +@creator_procs that could be used to create the remaining part +of the path.

    +
+ +

The all function is then trivially implemented on top of this:

+ +
def all(node,options={})
+  raise "options not a hash" unless Hash===options
+  if options[:create_new]
+    return [ @creator_procs[-1].call(node,true) ]
+  else
+    last_nodes,rest_creator = catch(:not_found) do
+      return @reader_proc.call([node])
+    end
+    if options[:ensure_created]
+      [ rest_creator.call(last_nodes[0],false) ]
+    else
+      []
+    end
+  end
+end
+
+ +

…and first, create_new etc. are even more trivial +frontends to that.

+ +

The implementations of the @creator_procs look like this:

+ +
@creator_procs[0] =
+  proc{|node,create_new| node}
+
+@creator_procs[1] =
+  proc {|node,create_new|
+    @creator_procs[0].call(Accessors.create_subnode_by_<thingsx>(node,create_new,<thingsx>),
+                           create_new)
+  }
+
+@creator_procs[2] =
+  proc {|node,create_new|
+    @creator_procs[1].call(Accessors.create_subnode_by_<thingsx-1>(node,create_new,<thingsx-1>),
+                           create_new)
+  }
+
+...
+
+@creator_procs[n] =
+  proc {|node,create_new|
+    @creator_procs[n-1].call(Accessors.create_subnode_by_<things[x+1-n]>(node,create_new,<things[x+1-n]>),
+                             create_new)
+  }
+
+...
+@creator_procs[x] =
+  proc {|node,create_new|
+    @creator_procs[x-1].call(Accessors.create_subnode_by_<things1>(node,create_new,<things1>),
+                             create_new)
+  }
+ +

..and the implementation of @reader_proc looks like this:

+ +
@reader_proc = rpx where
+
+rp0 = proc {|nodes| nodes}
+
+rp1 = proc {|nodes|
+              next_nodes = Accessors.subnodes_by_<thingsx>(nodes,<thingsx>)
+              if (next_nodes == [])
+                throw :not_found, [nodes,@creator_procs[1]]
+              else
+                rp0.call(next_nodes)
+              end
+            }
+
+rp2 = proc {|nodes|
+              next_nodes = Accessors.subnodes_by_<thingsx-1>(nodes,<thingsx-1>)
+              if (next_nodes == [])
+                throw :not_found, [nodes,@creator_procs[2]]
+              else
+                rp1.call(next_nodes)
+              end
+            }
+...
+
+rpx = proc {|nodes|
+              next_nodes = Accessors.subnodes_by_<things1>(nodes,<things1>)
+              if (next_nodes == [])
+                throw :not_found, [nodes,@creator_procs[x]]
+              else
+                rpx-1.call(next_nodes)
+              end
+            }
+
+ + + + + diff --git a/examples/README b/examples/README deleted file mode 100644 index 7ae907d..0000000 --- a/examples/README +++ /dev/null @@ -1,5 +0,0 @@ -This directory contains the example code snippets from the user manual -file (each time the documentation is regenerated, they're included in -the user manual, executed, and their output is checked for correctness -and included in the manual as well). So, if you've read the user -manual, you've already seen all the examples from this directory. diff --git a/examples/cleanup.rb b/examples/cleanup.rb deleted file mode 100644 index 04311ee..0000000 --- a/examples/cleanup.rb +++ /dev/null @@ -1,11 +0,0 @@ -%w{Address Client Company Customer Document Entry Folder Foo Item Order People Person Publication Signature}.each do |cname| - begin - Object.send(:remove_const, cname) # name clash with company_usage... - rescue - end -end - - -%w{company documents_folders order order_signature_enhanced stringarray time_node}.each do |mod| - $".delete_if{|f| f =~ %r{/#{mod}.rb$} } -end diff --git a/examples/company.rb b/examples/company.rb deleted file mode 100644 index fdf0e48..0000000 --- a/examples/company.rb +++ /dev/null @@ -1,34 +0,0 @@ -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 diff --git a/examples/company.xml b/examples/company.xml deleted file mode 100644 index e09a09a..0000000 --- a/examples/company.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - -
- Berlin - 10113 -
- - - - - James Kirk - - - - Ernie - - - - Bert - - - - -
diff --git a/examples/company_usage.intin.rb b/examples/company_usage.intin.rb deleted file mode 100644 index 4085145..0000000 --- a/examples/company_usage.intin.rb +++ /dev/null @@ -1,18 +0,0 @@ -#:invisible: -$:.unshift "../lib" - -load "cleanup.rb" - -require 'company' #<= -#:visible: -c = Company.load_from_file('company.xml') #<= -c.name #<= -c.customers.size #<= -c.customers[1] #<= -c.customers[1].name #<= -c.customers[0].name #<= -c.customers[0].name = 'James Tiberius Kirk' #<= -c.customers << Customer.new('cm','Cookie Monster') #<= -xml2 = c.save_to_xml #<= -#:invisible_retval: -xml2.write($stdout,2) #<= diff --git a/examples/documents_folders.rb b/examples/documents_folders.rb deleted file mode 100644 index cc07572..0000000 --- a/examples/documents_folders.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'xml/mapping' - -class Entry - include XML::Mapping - - text_node :name, "@name" -end - - -class Document [] - - def [](name) - entries.select{|e|e.name==name}[0] - end - - def append(name,entry) - entries << entry - entry.name = name - entry - end -end diff --git a/examples/documents_folders.xml b/examples/documents_folders.xml deleted file mode 100644 index 37f9e32..0000000 --- a/examples/documents_folders.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - inhale, exhale - - - - - - foo bar baz - - - - - diff --git a/examples/documents_folders_usage.intin.rb b/examples/documents_folders_usage.intin.rb deleted file mode 100644 index 383c82b..0000000 --- a/examples/documents_folders_usage.intin.rb +++ /dev/null @@ -1,18 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'documents_folders' #<= -#:visible: - -root = XML::Mapping.load_object_from_file "documents_folders.xml" #<= -root.name #<= -root.entries #<= - -root.append "etc", Folder.new -root["etc"].append "passwd", Document.new -root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh" -root["etc"].append "hosts", Document.new -root["etc"]["hosts"].contents = "127.0.0.1 localhost" - -xml = root.save_to_xml #<= -#:invisible_retval: -xml.write $stdout,2 diff --git a/examples/order.rb b/examples/order.rb deleted file mode 100644 index 41c7c68..0000000 --- a/examples/order.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'xml/mapping' - -## forward declarations -class Client; end -class Address; end -class Item; end -class Signature; end - - -class Order - include XML::Mapping - - text_node :reference, "@reference" - object_node :client, "Client", :class=>Client - hash_node :items, "Item", "@reference", :class=>Item - array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] - - def total_price - items.values.map{|i| i.total_price}.inject(0){|x,y|x+y} - end -end - - -class Client - include XML::Mapping - - text_node :name, "Name" - object_node :home_address, "Address[@where='home']", :class=>Address - object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil -end - - -class Address - include XML::Mapping - - text_node :city, "City" - text_node :state, "State" - numeric_node :zip, "ZIP" - text_node :street, "Street" -end - - -class Item - include XML::Mapping - - text_node :descr, "Description" - numeric_node :quantity, "Quantity" - numeric_node :unit_price, "UnitPrice" - - def total_price - quantity*unit_price - end -end - - -class Signature - include XML::Mapping - - text_node :name, "Name" - text_node :position, "Position", :default_value=>"Some Employee" -end diff --git a/examples/order.xml b/examples/order.xml deleted file mode 100644 index ce79c0d..0000000 --- a/examples/order.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Jean Smith -
- San Mateo - CA - 94403 - 2000, Alameda de las Pulgas -
-
- San Francisco - CA - 94102 - 98765, Fulton Street -
-
- - - Stuffed Penguin - 10 - 8.95 - - - - Chocolate - 5 - 28.50 - - - - Cookie - 30 - 0.85 - - - - - John Doe - product manager - - - - Jill Smith - clerk - - - - Miles O'Brien - - - -
diff --git a/examples/order_signature_enhanced.rb b/examples/order_signature_enhanced.rb deleted file mode 100644 index ff457b9..0000000 --- a/examples/order_signature_enhanced.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Signature - include XML::Mapping - - text_node :name, "Name" - text_node :position, "Position", :default_value=>"Some Employee" - time_node :signed_on, "signed-on", :default_value=>Time.now -end diff --git a/examples/order_signature_enhanced.xml b/examples/order_signature_enhanced.xml deleted file mode 100644 index 8491465..0000000 --- a/examples/order_signature_enhanced.xml +++ /dev/null @@ -1,9 +0,0 @@ - - John Doe - product manager - - 13 - 2 - 2005 - - diff --git a/examples/order_signature_enhanced_usage.intin.rb b/examples/order_signature_enhanced_usage.intin.rb deleted file mode 100644 index 8f8534e..0000000 --- a/examples/order_signature_enhanced_usage.intin.rb +++ /dev/null @@ -1,12 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'xml/mapping' -require 'time_node.rb' -require 'order' -require 'order_signature_enhanced' -#<= -#:visible: -s=Signature.load_from_file("order_signature_enhanced.xml") #<= -s.signed_on #<= -s.signed_on=Time.local(1976,12,18) #<= -s.save_to_xml.write($stdout,2) #<= diff --git a/examples/order_usage.intin.rb b/examples/order_usage.intin.rb deleted file mode 100644 index a121ce4..0000000 --- a/examples/order_usage.intin.rb +++ /dev/null @@ -1,118 +0,0 @@ -#:invisible: -$:.unshift "../lib" -load "cleanup.rb" - -require 'order' - -require 'xml/xxpath_methods' - -require 'test/unit/assertions' -include Test::Unit::Assertions #<= -#:visible: -####read access -o=Order.load_from_file("order.xml") #<= -o.reference #<= -o.client #<= -o.items.keys #<= -o.items["RF-0034"].descr #<= -o.items["RF-0034"].total_price #<= -o.signatures #<= -o.signatures[2].name #<= -o.signatures[2].position #<= -## default value was set - -o.total_price #<= - -#:invisible: -assert_equal "12343-AHSHE-314159", o.reference -assert_equal "Jean Smith", o.client.name -assert_equal "San Francisco", o.client.work_address.city -assert_equal "San Mateo", o.client.home_address.city -assert_equal %w{RF-0001 RF-0034 RF-3341}, o.items.keys.sort -assert_equal ['John Doe','Jill Smith','Miles O\'Brien'], o.signatures.map{|s|s.name} -assert_equal 2575, (10 * o.total_price).round -#<= -#:visible: - -####write access -o.client.name="James T. Kirk" -o.items['RF-4711'] = Item.new -o.items['RF-4711'].descr = 'power transfer grid' -o.items['RF-4711'].quantity = 2 -o.items['RF-4711'].unit_price = 29.95 - -s=Signature.new -s.name='Harry Smith' -s.position='general manager' -o.signatures << s -xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) #<= -#:invisible_retval: -xml.write($stdout,2) #<= - -#:invisible: -assert_equal %w{RF-0001 RF-0034 RF-3341 RF-4711}, xml.all_xpath("Item/@reference").map{|x|x.text}.sort -assert_equal ['John Doe','Jill Smith','Miles O\'Brien','Harry Smith'], - xml.all_xpath("Signed-By/Signature/Name").map{|x|x.text} -#<= -#:visible: - - -#<= -#:visible_retval: -####Starting a new order from scratch -o = Order.new #<= -## attributes with default values (here: signatures) are set -## automatically - -#:handle_exceptions: -xml=o.save_to_xml #<= -#:no_exceptions: -## can't save as long as there are still unset attributes without -## default values - -o.reference = "FOOBAR-1234" - -o.client = Client.new -o.client.name = 'Ford Prefect' -o.client.home_address = Address.new -o.client.home_address.street = '42 Park Av.' -o.client.home_address.city = 'small planet' -o.client.home_address.zip = 17263 -o.client.home_address.state = 'Betelgeuse system' - -o.items={'XY-42' => Item.new} -o.items['XY-42'].descr = 'improbability drive' -o.items['XY-42'].quantity = 3 -o.items['XY-42'].unit_price = 299.95 - -#:invisible_retval: -xml=o.save_to_xml -xml.write($stdout,2) -#<= -#:invisible: -assert_equal "order", xml.name -assert_equal o.reference, xml.first_xpath("@reference").text -assert_equal o.client.name, xml.first_xpath("Client/Name").text -assert_equal o.client.home_address.street, xml.first_xpath("Client/Address[@where='home']/Street").text -assert_equal o.client.home_address.city, xml.first_xpath("Client/Address[@where='home']/City").text -assert_nil xml.first_xpath("Client/Address[@where='work']", :allow_nil=>true) -assert_equal 1, xml.all_xpath("Client/Address").size - -o.client.work_address = Address.new -o.client.work_address.street = 'milky way 2' -o.client.work_address.city = 'Ursa Major' -o.client.work_address.zip = 18293 -o.client.work_address.state = 'Magellan Cloud' -xml=o.save_to_xml - -assert_equal o.client.work_address.street, xml.first_xpath("Client/Address[@where='work']/Street").text -assert_equal o.client.work_address.city, xml.first_xpath("Client/Address[@where='work']/City").text -assert_equal o.client.home_address.street, xml.first_xpath("Client/Address[@where='home']/Street").text -assert_equal 2, xml.all_xpath("Client/Address").size -#<= -#:visible: - -## the root element name when saving an object to XML will by default -## be derived from the class name (in this example, "Order" became -## "order"). This can be overridden on a per-class basis; see -## XML::Mapping::ClassMethods#root_element_name for details. diff --git a/examples/person.intin.rb b/examples/person.intin.rb deleted file mode 100644 index 83b5a6d..0000000 --- a/examples/person.intin.rb +++ /dev/null @@ -1,44 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'xml/mapping' -require 'xml/xxpath_methods' -#<= -#:visible: - -class Person - include XML::Mapping - - choice_node :if, 'name', :then, (text_node :name, 'name'), - :elsif, '@name', :then, (text_node :name, '@name'), - :else, (text_node :name, '.') -end - -### usage - -p1 = Person.load_from_xml(REXML::Document.new('').root)#<= - -p2 = Person.load_from_xml(REXML::Document.new('James').root)#<= - -p3 = Person.load_from_xml(REXML::Document.new('Suzy').root)#<= - - -#:invisible_retval: -p1.save_to_xml.write($stdout)#<= - -p2.save_to_xml.write($stdout)#<= - -p3.save_to_xml.write($stdout)#<= - -#:invisible: -require 'test/unit/assertions' -include Test::Unit::Assertions - -assert_equal "Jim", p1.name -assert_equal "James", p2.name -assert_equal "Suzy", p3.name - -xml = p3.save_to_xml -assert_equal "name", xml.elements[1].name -assert_equal "Suzy", xml.elements[1].text - -#<= diff --git a/examples/person_mm.intin.rb b/examples/person_mm.intin.rb deleted file mode 100644 index 8eabb46..0000000 --- a/examples/person_mm.intin.rb +++ /dev/null @@ -1,119 +0,0 @@ -#:invisible: -$:.unshift "../lib" -begin - Object.send(:remove_const, "Address") # remove any previous definitions - Object.send(:remove_const, "Person") # remove any previous definitions -rescue -end -#<= -#:visible: -require 'xml/mapping' - -class Address; end - -class Person - include XML::Mapping - - # the default mapping. Stores the name and age in XML attributes, - # and the address in a sub-element "address". - - text_node :name, "@name" - numeric_node :age, "@age" - object_node :address, "address", :class=>Address - - use_mapping :other - - # the ":other" mapping. Non-default root element name; name and age - # stored in XML elements; address stored in the person's element - # itself - - root_element_name "individual" - text_node :name, "name" - numeric_node :age, "age" - object_node :address, ".", :class=>Address - - # you could also specify the mapping on a per-node basis with the - # :mapping option, e.g.: - # - # numeric_node :age, "age", :mapping=>:other -end - - -class Address - include XML::Mapping - - # the default mapping. - - text_node :street, "street" - numeric_node :number, "number" - text_node :city, "city" - numeric_node :zip, "zip" - - use_mapping :other - - # the ":other" mapping. - - text_node :street, "street-name" - numeric_node :number, "street-name/@number" - text_node :city, "city-name" - numeric_node :zip, "city-name/@zip-code" -end - - -### usage - -## XML representation of a person in the default mapping -xml = REXML::Document.new(' - -
- Abbey Road - 72 - London - 18827 -
-
').root - -## load using the default mapping -p = Person.load_from_xml xml #<= - -#:invisible_retval: -## save using the default mapping -xml2 = p.save_to_xml -xml2.write $stdout,2 #<= - -## xml2 identical to xml - - -## now, save the same person to XML using the :other mapping... -other_xml = p.save_to_xml :mapping=>:other -other_xml.write $stdout,2 #<= - -#:visible_retval: -## load it again using the :other mapping -p2 = Person.load_from_xml other_xml, :mapping=>:other #<= - -#:invisible_retval: -## p2 identical to p #<= - -#:invisible: -require 'test/unit/assertions' -include Test::Unit::Assertions - -require 'xml/xxpath_methods' - -assert_equal "Suzy", p.name -assert_equal 28, p.age -assert_equal "Abbey Road", p.address.street -assert_equal 72, p.address.number -assert_equal "London", p.address.city -assert_equal 18827, p.address.zip - -assert_equal "individual", other_xml.name -assert_equal p.name, other_xml.first_xpath("name").text -assert_equal p.age, other_xml.first_xpath("age").text.to_i -assert_equal p.address.street, other_xml.first_xpath("street-name").text -assert_equal p.address.number, other_xml.first_xpath("street-name/@number").text.to_i -assert_equal p.address.city, other_xml.first_xpath("city-name").text -assert_equal p.address.zip, other_xml.first_xpath("city-name/@zip-code").text.to_i - -#<= diff --git a/examples/publication.intin.rb b/examples/publication.intin.rb deleted file mode 100644 index e5882cc..0000000 --- a/examples/publication.intin.rb +++ /dev/null @@ -1,44 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'xml/mapping' -require 'xml/xxpath_methods' -#<= -#:visible: - -class Publication - include XML::Mapping - - choice_node :if, '@author', :then, (text_node :author, '@author'), - :elsif, 'contr', :then, (array_node :contributors, 'contr', :class=>String) -end - -### usage - -p1 = Publication.load_from_xml(REXML::Document.new('').root)#<= - -p2 = Publication.load_from_xml(REXML::Document.new(' - - Chris - Mel - Toby -').root)#<= - -#:invisible: -require 'test/unit/assertions' -include Test::Unit::Assertions - -assert_equal "Jim", p1.author -assert_nil p1.contributors - -assert_nil p2.author -assert_equal ["Chris", "Mel", "Toby"], p2.contributors - -xml1 = p1.save_to_xml -xml2 = p2.save_to_xml - -assert_equal p1.author, xml1.first_xpath("@author").text -assert_nil xml1.first_xpath("contr", :allow_nil=>true) - -assert_nil xml2.first_xpath("@author", :allow_nil=>true) -assert_equal p2.contributors, xml2.all_xpath("contr").map{|elt|elt.text} -#<= diff --git a/examples/reader.intin.rb b/examples/reader.intin.rb deleted file mode 100644 index f504e66..0000000 --- a/examples/reader.intin.rb +++ /dev/null @@ -1,33 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'xml/mapping' -require 'xml/xxpath_methods' -#<= -#:visible: - -class Foo - include XML::Mapping - - text_node :name, "@name", :reader=>proc{|obj,xml,default_reader| - default_reader.call(obj,xml) - obj.name += xml.attributes['more'] - }, - :writer=>proc{|obj,xml| - xml.attributes['bar'] = "hi #{obj.name} ho" - } -end - -f = Foo.load_from_xml(REXML::Document.new('').root)#<= - -#:invisible_retval: -xml = f.save_to_xml -xml.write $stdout,2 #<= - -#:invisible: -require 'test/unit/assertions' -include Test::Unit::Assertions - -assert_equal "JimXYZ", f.name -assert_equal "hi JimXYZ ho", xml.attributes['bar'] - -#<= diff --git a/examples/stringarray.rb b/examples/stringarray.rb deleted file mode 100644 index fb0873a..0000000 --- a/examples/stringarray.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'xml/mapping' -class People - include XML::Mapping - array_node :names, "names", "name", :class=>String -end diff --git a/examples/stringarray.xml b/examples/stringarray.xml deleted file mode 100644 index 864ee52..0000000 --- a/examples/stringarray.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Jim - Susan - Herbie - Nancy - - diff --git a/examples/stringarray_usage.intin.rb b/examples/stringarray_usage.intin.rb deleted file mode 100644 index 38b242d..0000000 --- a/examples/stringarray_usage.intin.rb +++ /dev/null @@ -1,11 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'stringarray' #<= -#:visible: -ppl=People.load_from_file("stringarray.xml") #<= -ppl.names #<= - -ppl.names.concat ["Mary","Arnold"] #<= -#:invisible_retval: -ppl.save_to_xml.write $stdout,2 -#<= diff --git a/examples/time_augm.intin.rb b/examples/time_augm.intin.rb deleted file mode 100644 index fe032e6..0000000 --- a/examples/time_augm.intin.rb +++ /dev/null @@ -1,19 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'order' #<= -#:visible: -class Time - include XML::Mapping - - numeric_node :year, "year" - numeric_node :month, "month" - numeric_node :day, "mday" - numeric_node :hour, "hours" - numeric_node :min, "minutes" - numeric_node :sec, "seconds" -end - - -nowxml=Time.now.save_to_xml #<= -#:invisible_retval: -nowxml.write($stdout,2)#<= diff --git a/examples/time_augm_loading.intin.rb b/examples/time_augm_loading.intin.rb deleted file mode 100644 index c81f1df..0000000 --- a/examples/time_augm_loading.intin.rb +++ /dev/null @@ -1,44 +0,0 @@ -#:invisible: -$:.unshift "../lib" -require 'xml/mapping' -require 'xml/xxpath_methods' - -class Time - include XML::Mapping - - numeric_node :year, "year" - numeric_node :month, "month" - numeric_node :day, "mday" - numeric_node :hour, "hours" - numeric_node :min, "minutes" - numeric_node :sec, "seconds" -end - -#<= -#:invisible_retval: -#:visible: - -def Time.load_from_xml(xml, options={:mapping=>:_default}) - year,month,day,hour,min,sec = - [xml.first_xpath("year").text.to_i, - xml.first_xpath("month").text.to_i, - xml.first_xpath("mday").text.to_i, - xml.first_xpath("hours").text.to_i, - xml.first_xpath("minutes").text.to_i, - xml.first_xpath("seconds").text.to_i] - Time.local(year,month,day,hour,min,sec) -end -#<= -#:invisible: -require 'test/unit/assertions' -include Test::Unit::Assertions - -t = Time.now -t2 = Time.load_from_xml(t.save_to_xml) - -assert_equal t.year, t2.year -assert_equal t.month, t2.month -assert_equal t.day, t2.day -assert_equal t.hour, t2.hour -assert_equal t.min, t2.min -assert_equal t.sec, t2.sec diff --git a/examples/time_node.rb b/examples/time_node.rb deleted file mode 100644 index 02a0bdb..0000000 --- a/examples/time_node.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'xml/mapping/base' - -class TimeNode < XML::Mapping::SingleAttributeNode - def initialize(*args) - path,*args = super(*args) - @y_path = XML::XXPath.new(path+"/year") - @m_path = XML::XXPath.new(path+"/month") - @d_path = XML::XXPath.new(path+"/day") - args - end - - def extract_attr_value(xml) - y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i, - @m_path.first(xml).text.to_i, - @d_path.first(xml).text.to_i] - } - Time.local(y,m,d) - end - - def set_attr_value(xml, value) - @y_path.first(xml,:ensure_created=>true).text = value.year - @m_path.first(xml,:ensure_created=>true).text = value.month - @d_path.first(xml,:ensure_created=>true).text = value.day - end -end - - -XML::Mapping.add_node_class TimeNode diff --git a/examples/time_node_w_marshallers.intin.rb b/examples/time_node_w_marshallers.intin.rb deleted file mode 100644 index 7ff8253..0000000 --- a/examples/time_node_w_marshallers.intin.rb +++ /dev/null @@ -1,48 +0,0 @@ -#:invisible: -#:invisible_retval: -$:.unshift "../lib" #<= -#:visible: -require 'xml/mapping' -require 'xml/xxpath_methods' - -class Signature - include XML::Mapping - - text_node :name, "Name" - text_node :position, "Position", :default_value=>"Some Employee" - object_node :signed_on, "signed-on", - :unmarshaller=>proc{|xml| - y,m,d = [xml.first_xpath("year").text.to_i, - xml.first_xpath("month").text.to_i, - xml.first_xpath("day").text.to_i] - Time.local(y,m,d) - }, - :marshaller=>proc{|xml,value| - e = xml.elements.add; e.name = "year"; e.text = value.year - e = xml.elements.add; e.name = "month"; e.text = value.month - e = xml.elements.add; e.name = "day"; e.text = value.day - - # xml.first("year",:ensure_created=>true).text = value.year - # xml.first("month",:ensure_created=>true).text = value.month - # xml.first("day",:ensure_created=>true).text = value.day - } -end #<= -#:invisible: -require 'test/unit/assertions' - -include Test::Unit::Assertions - -t=Time.local(2005,12,1) - -s=Signature.new -s.name = "Olaf Klischat"; s.position="chief"; s.signed_on=t -xml = s.save_to_xml - -assert_equal "2005", xml.first_xpath("signed-on/year").text -assert_equal "12", xml.first_xpath("signed-on/month").text -assert_equal "1", xml.first_xpath("signed-on/day").text - -s2 = Signature.load_from_xml xml -assert_equal "Olaf Klischat", s2.name -assert_equal "chief", s2.position -assert_equal t, s2.signed_on diff --git a/examples/time_node_w_marshallers.xml b/examples/time_node_w_marshallers.xml deleted file mode 100644 index 8491465..0000000 --- a/examples/time_node_w_marshallers.xml +++ /dev/null @@ -1,9 +0,0 @@ - - John Doe - product manager - - 13 - 2 - 2005 - - diff --git a/examples/xpath_create_new.intin.rb b/examples/xpath_create_new.intin.rb deleted file mode 100644 index edfbe31..0000000 --- a/examples/xpath_create_new.intin.rb +++ /dev/null @@ -1,85 +0,0 @@ -#:invisible: -$:.unshift "../lib" #<= -#:visible: -require 'xml/xxpath' - -d=REXML::Document.new < - - Java - Ruby - - -EOS - - -rootelt=d.root - -#:invisible_retval: -path1=XML::XXPath.new("/bar/baz[@key='work']") - -#:visible_retval: -path1.create_new(rootelt)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### a new element is created for *each* path element, regardless of -### what existed before. So a new "bar" element was added, with a new -### "baz" element inside it - -### same call again... -#:visible_retval: -path1.create_new(rootelt)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### same procedure -- new elements added for each path element - - -#:visible_retval: -## get reference to 1st "baz" element -firstbazelt=XML::XXPath.new("/bar/baz").first(rootelt)#<= - -#:invisible_retval: -path2=XML::XXPath.new("@key2") - -#:visible_retval: -path2.create_new(firstbazelt)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### ok, new attribute node added - -### same call again... -#:visible_retval: -#:handle_exceptions: -path2.create_new(firstbazelt)#<= -#:no_exceptions: -### can't create that path anew again -- an element can't have more -### than one attribute with the same name - -#:invisible_retval: -### the document hasn't changed -d.write($stdout,2)#<= - - - -### create_new the same path as in the ensure_created example -#:visible_retval: -baz6elt=XML::XXPath.new("/bar/baz[6]").create_new(rootelt)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### ok, new "bar" element and 6th "baz" element inside it created - - -#:visible_retval: -#:handle_exceptions: -XML::XXPath.new("baz[6]").create_new(baz6elt.parent)#<= -#:no_exceptions: -#:invisible_retval: -### yep, baz[6] already existed and thus couldn't be created once -### again - -### but of course... -#:visible_retval: -XML::XXPath.new("/bar/baz[6]").create_new(rootelt)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### this works because *all* path elements are newly created diff --git a/examples/xpath_docvsroot.intin.rb b/examples/xpath_docvsroot.intin.rb deleted file mode 100644 index dbd9f68..0000000 --- a/examples/xpath_docvsroot.intin.rb +++ /dev/null @@ -1,30 +0,0 @@ -#:invisible: -$:.unshift "../lib" #<= -#:visible: -require 'xml/xxpath' - -d=REXML::Document.new < - - - pingpong - - - - -EOS - -XML::XXPath.new("/foo/bar").all(d)#<= - -XML::XXPath.new("/bar").all(d)#<= - -XML::XXPath.new("/foo/bar").all(d.root)#<= - -XML::XXPath.new("/bar").all(d.root)#<= - - -firstelt = XML::XXPath.new("/foo/bar/first").first(d)#<= - -XML::XXPath.new("/first/second").all(firstelt)#<= - -XML::XXPath.new("/second").all(firstelt)#<= diff --git a/examples/xpath_ensure_created.intin.rb b/examples/xpath_ensure_created.intin.rb deleted file mode 100644 index a7ce60e..0000000 --- a/examples/xpath_ensure_created.intin.rb +++ /dev/null @@ -1,62 +0,0 @@ -#:invisible: -$:.unshift "../lib" #<= -#:visible: -require 'xml/xxpath' - -d=REXML::Document.new < - - Java - Ruby - - -EOS - - -rootelt=d.root - -#### ensuring that a specific path exists inside the document - -#:visible_retval: -XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### no change (path existed before) - - -#:visible_retval: -XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### path was added - -#:visible_retval: -XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### no change this time - -#:visible_retval: -XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### this fit in the 1st "baz" element since -### there was no "key2" attribute there before. - -#:visible_retval: -XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### no change - -#:visible_retval: -XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements - -#:visible_retval: -XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)#<= -#:invisible_retval: -d.write($stdout,2)#<= -### no change this time diff --git a/examples/xpath_pathological.intin.rb b/examples/xpath_pathological.intin.rb deleted file mode 100644 index 184a63e..0000000 --- a/examples/xpath_pathological.intin.rb +++ /dev/null @@ -1,42 +0,0 @@ -#:invisible: -$:.unshift "../lib" #<= -#:visible: -require 'xml/xxpath' - -d=REXML::Document.new < - - - -EOS - - -rootelt=d.root - - -XML::XXPath.new("*").all(rootelt)#<= -### ok - -XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true)#<= -### ok, nothing there - -### the same call with :ensure_created=>true -newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)#<= - -#:invisible_retval: -d.write($stdout,2)#<= - -#:visible_retval: -### a new "unspecified" element was created -newelt.unspecified?#<= - -### we must modify it to "specify" it -newelt.name="new-one" -newelt.text="hello!" -newelt.unspecified?#<= - -#:invisible_retval: -d.write($stdout,2)#<= - -### you could also set unspecified to false explicitly, as in: -newelt.unspecified=true diff --git a/examples/xpath_usage.intin.rb b/examples/xpath_usage.intin.rb deleted file mode 100644 index 169db2d..0000000 --- a/examples/xpath_usage.intin.rb +++ /dev/null @@ -1,51 +0,0 @@ -#:invisible: -$:.unshift "../lib" #<= -#:visible: -require 'xml/xxpath' - -d=REXML::Document.new < - - Java - Ruby - - - hello - scrabble - goodbye - - - poker - - -EOS - - -####read access -path=XML::XXPath.new("/foo/bar[2]/baz") - -## path.all(document) gives all elements matching path in document -path.all(d)#<= - -## loop over them -path.each(d){|elt| puts elt.text}#<= - -## the first of those -path.first(d)#<= - -## no match here (only three "baz" elements) -path2=XML::XXPath.new("/foo/bar[2]/baz[4]") -path2.all(d)#<= - -#:handle_exceptions: -## "first" raises XML::XXPathError in such cases... -path2.first(d)#<= -#:no_exceptions: - -##...unless we allow nil returns -path2.first(d,:allow_nil=>true)#<= - -##attribute nodes can also be returned -keysPath=XML::XXPath.new("/foo/*/*/@key") - -keysPath.all(d).map{|attr|attr.text}#<= diff --git a/fonts/Lato-Light.ttf b/fonts/Lato-Light.ttf new file mode 100644 index 0000000..b49dd43 Binary files /dev/null and b/fonts/Lato-Light.ttf differ diff --git a/fonts/Lato-LightItalic.ttf b/fonts/Lato-LightItalic.ttf new file mode 100644 index 0000000..7959fef Binary files /dev/null and b/fonts/Lato-LightItalic.ttf differ diff --git a/fonts/Lato-Regular.ttf b/fonts/Lato-Regular.ttf new file mode 100644 index 0000000..839cd58 Binary files /dev/null and b/fonts/Lato-Regular.ttf differ diff --git a/fonts/Lato-RegularItalic.ttf b/fonts/Lato-RegularItalic.ttf new file mode 100644 index 0000000..bababa0 Binary files /dev/null and b/fonts/Lato-RegularItalic.ttf differ diff --git a/fonts/SourceCodePro-Bold.ttf b/fonts/SourceCodePro-Bold.ttf new file mode 100644 index 0000000..61e3090 Binary files /dev/null and b/fonts/SourceCodePro-Bold.ttf differ diff --git a/fonts/SourceCodePro-Regular.ttf b/fonts/SourceCodePro-Regular.ttf new file mode 100644 index 0000000..85686d9 Binary files /dev/null and b/fonts/SourceCodePro-Regular.ttf differ diff --git a/images/add.png b/images/add.png new file mode 100644 index 0000000..6332fef Binary files /dev/null and b/images/add.png differ diff --git a/images/arrow_up.png b/images/arrow_up.png new file mode 100644 index 0000000..1ebb193 Binary files /dev/null and b/images/arrow_up.png differ diff --git a/images/brick.png b/images/brick.png new file mode 100644 index 0000000..7851cf3 Binary files /dev/null and b/images/brick.png differ diff --git a/images/brick_link.png b/images/brick_link.png new file mode 100644 index 0000000..9ebf013 Binary files /dev/null and b/images/brick_link.png differ diff --git a/images/bug.png b/images/bug.png new file mode 100644 index 0000000..2d5fb90 Binary files /dev/null and b/images/bug.png differ diff --git a/images/bullet_black.png b/images/bullet_black.png new file mode 100644 index 0000000..5761970 Binary files /dev/null and b/images/bullet_black.png differ diff --git a/images/bullet_toggle_minus.png b/images/bullet_toggle_minus.png new file mode 100644 index 0000000..b47ce55 Binary files /dev/null and b/images/bullet_toggle_minus.png differ diff --git a/images/bullet_toggle_plus.png b/images/bullet_toggle_plus.png new file mode 100644 index 0000000..9ab4a89 Binary files /dev/null and b/images/bullet_toggle_plus.png differ diff --git a/images/date.png b/images/date.png new file mode 100644 index 0000000..783c833 Binary files /dev/null and b/images/date.png differ diff --git a/images/delete.png b/images/delete.png new file mode 100644 index 0000000..08f2493 Binary files /dev/null and b/images/delete.png differ diff --git a/images/find.png b/images/find.png new file mode 100644 index 0000000..1547479 Binary files /dev/null and b/images/find.png differ diff --git a/images/loadingAnimation.gif b/images/loadingAnimation.gif new file mode 100644 index 0000000..82290f4 Binary files /dev/null and b/images/loadingAnimation.gif differ diff --git a/images/macFFBgHack.png b/images/macFFBgHack.png new file mode 100644 index 0000000..c6473b3 Binary files /dev/null and b/images/macFFBgHack.png differ diff --git a/images/package.png b/images/package.png new file mode 100644 index 0000000..da3c2a2 Binary files /dev/null and b/images/package.png differ diff --git a/images/page_green.png b/images/page_green.png new file mode 100644 index 0000000..de8e003 Binary files /dev/null and b/images/page_green.png differ diff --git a/images/page_white_text.png b/images/page_white_text.png new file mode 100644 index 0000000..813f712 Binary files /dev/null and b/images/page_white_text.png differ diff --git a/images/page_white_width.png b/images/page_white_width.png new file mode 100644 index 0000000..1eb8809 Binary files /dev/null and b/images/page_white_width.png differ diff --git a/images/plugin.png b/images/plugin.png new file mode 100644 index 0000000..6187b15 Binary files /dev/null and b/images/plugin.png differ diff --git a/images/ruby.png b/images/ruby.png new file mode 100644 index 0000000..f763a16 Binary files /dev/null and b/images/ruby.png differ diff --git a/images/tag_blue.png b/images/tag_blue.png new file mode 100644 index 0000000..3f02b5f Binary files /dev/null and b/images/tag_blue.png differ diff --git a/images/tag_green.png b/images/tag_green.png new file mode 100644 index 0000000..83ec984 Binary files /dev/null and b/images/tag_green.png differ diff --git a/images/transparent.png b/images/transparent.png new file mode 100644 index 0000000..d665e17 Binary files /dev/null and b/images/transparent.png differ diff --git a/images/wrench.png b/images/wrench.png new file mode 100644 index 0000000..5c8213f Binary files /dev/null and b/images/wrench.png differ diff --git a/images/wrench_orange.png b/images/wrench_orange.png new file mode 100644 index 0000000..565a933 Binary files /dev/null and b/images/wrench_orange.png differ diff --git a/images/zoom.png b/images/zoom.png new file mode 100644 index 0000000..908612e Binary files /dev/null and b/images/zoom.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..23d3a40 --- /dev/null +++ b/index.html @@ -0,0 +1,2093 @@ + + + + + + +XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ + +

XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter

+ +

Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.

+ +

Download

+ +

For downloading the latest version, git repository access etc. go to:

+ +

github.com/multi-io/xml-mapping

+ +

Contents of this Document

+ + +

Example

+ +

(example document stolen + extended from www.castor.org/xml-mapping.html)

+ +

Input Document:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<Order reference="12343-AHSHE-314159">
+  <Client>
+    <Name>Jean Smith</Name>
+    <Address where="home">
+      <City>San Mateo</City>
+      <State>CA</State>
+      <ZIP>94403</ZIP>
+      <Street>2000, Alameda de las Pulgas</Street>
+    </Address>
+    <Address where="work">
+      <City>San Francisco</City>
+      <State>CA</State>
+      <ZIP>94102</ZIP>
+      <Street>98765, Fulton Street</Street>
+    </Address>
+  </Client>
+
+  <Item reference="RF-0001">
+    <Description>Stuffed Penguin</Description>
+    <Quantity>10</Quantity>
+    <UnitPrice>8.95</UnitPrice>
+  </Item>
+
+  <Item reference="RF-0034">
+    <Description>Chocolate</Description>
+    <Quantity>5</Quantity>
+    <UnitPrice>28.50</UnitPrice>
+  </Item>
+
+  <Item reference="RF-3341">
+    <Description>Cookie</Description>
+    <Quantity>30</Quantity>
+    <UnitPrice>0.85</UnitPrice>
+  </Item>
+
+  <Signed-By>
+    <Signature>
+      <Name>John Doe</Name>
+      <Position>product manager</Position>
+    </Signature>
+
+    <Signature>
+      <Name>Jill Smith</Name>
+      <Position>clerk</Position>
+    </Signature>
+
+    <Signature>
+      <Name>Miles O'Brien</Name>
+    </Signature>
+  </Signed-By>
+
+</Order>
+ +

Mapping Class Declaration:

+ +
require 'xml/mapping'
+
+## forward declarations
+class Client; end
+class Address; end
+class Item; end
+class Signature; end
+
+
+class Order
+  include XML::Mapping
+
+  text_node :reference, "@reference"
+  object_node :client, "Client", :class=>Client
+  hash_node :items, "Item", "@reference", :class=>Item
+  array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
+
+  def total_price
+    items.values.map{|i| i.total_price}.inject(0){|x,y|x+y}
+  end
+end
+
+
+class Client
+  include XML::Mapping
+
+  text_node :name, "Name"
+  object_node :home_address, "Address[@where='home']", :class=>Address
+  object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil
+end
+
+
+class Address
+  include XML::Mapping
+
+  text_node :city, "City"
+  text_node :state, "State"
+  numeric_node :zip, "ZIP"
+  text_node :street, "Street"
+end
+
+
+class Item
+  include XML::Mapping
+
+  text_node :descr, "Description"
+  numeric_node :quantity, "Quantity"
+  numeric_node :unit_price, "UnitPrice"
+
+  def total_price
+    quantity*unit_price
+  end
+end
+
+
+class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+end
+
+ +

Usage:

+ +
####read access
+o=Order.load_from_file("order.xml") 
+=> #<Order:0x007ff64a0fe8b0 @signatures=[#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">], @reference="12343-AHSHE-314159", @client=#<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>, @items={"RF-0001"=>#<Item:0x007ff64a0df550 @descr="Stuffed Penguin", @quantity=10, @unit_price=8.95>, "RF-0034"=>#<Item:0x007ff64a0ddbd8 @descr="Chocolate", @quantity=5, @unit_price=28.5>, "RF-3341"=>#<Item:0x007ff64a0dc0d0 @descr="Cookie", @quantity=30, @unit_price=0.85>}>
+o.reference 
+=> "12343-AHSHE-314159"
+o.client 
+=> #<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>
+o.items.keys 
+=> ["RF-0001", "RF-0034", "RF-3341"]
+o.items["RF-0034"].descr 
+=> "Chocolate"
+o.items["RF-0034"].total_price 
+=> 142.5
+o.signatures 
+=> [#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">]
+o.signatures[2].name 
+=> "Miles O'Brien"
+o.signatures[2].position 
+=> "Some Employee"
+## default value was set
+
+o.total_price 
+=> 257.5
+
+####write access
+o.client.name="James T. Kirk"
+o.items['RF-4711'] = Item.new
+o.items['RF-4711'].descr = 'power transfer grid'
+o.items['RF-4711'].quantity = 2
+o.items['RF-4711'].unit_price = 29.95
+
+s=Signature.new
+s.name='Harry Smith'
+s.position='general manager'
+o.signatures << s
+xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) 
+=> <order reference='12343-AHSHE-314159'> ... </>
+xml.write($stdout,2) 
+<order reference='12343-AHSHE-314159'>
+  <Client>
+    <Name>
+      James T. Kirk
+    </Name>
+    <Address where='home'>
+      <City>
+        San Mateo
+      </City>
+      <State>
+        CA
+      </State>
+      <ZIP>
+        94403
+      </ZIP>
+      <Street>
+        2000, Alameda de las Pulgas
+      </Street>
+    </Address>
+    <Address where='work'>
+      <City>
+        San Francisco
+      </City>
+      <State>
+        CA
+      </State>
+      <ZIP>
+        94102
+      </ZIP>
+      <Street>
+        98765, Fulton Street
+      </Street>
+    </Address>
+  </Client>
+  <Item reference='RF-0001'>
+    <Description>
+      Stuffed Penguin
+    </Description>
+    <Quantity>
+      10
+    </Quantity>
+    <UnitPrice>
+      8.95
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-0034'>
+    <Description>
+      Chocolate
+    </Description>
+    <Quantity>
+      5
+    </Quantity>
+    <UnitPrice>
+      28.5
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-3341'>
+    <Description>
+      Cookie
+    </Description>
+    <Quantity>
+      30
+    </Quantity>
+    <UnitPrice>
+      0.85
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-4711'>
+    <Description>
+      power transfer grid
+    </Description>
+    <Quantity>
+      2
+    </Quantity>
+    <UnitPrice>
+      29.95
+    </UnitPrice>
+  </Item>
+  <Signed-By>
+    <Signature>
+      <Name>
+        John Doe
+      </Name>
+      <Position>
+        product manager
+      </Position>
+    </Signature>
+    <Signature>
+      <Name>
+        Jill Smith
+      </Name>
+      <Position>
+        clerk
+      </Position>
+    </Signature>
+    <Signature>
+      <Name>
+        Miles O&apos;Brien
+      </Name>
+    </Signature>
+    <Signature>
+      <Name>
+        Harry Smith
+      </Name>
+      <Position>
+        general manager
+      </Position>
+    </Signature>
+  </Signed-By>
+</order>
+
+
+####Starting a new order from scratch
+o = Order.new 
+=> #<Order:0x007ff64a206050 @signatures=[]>
+## attributes with default values (here: signatures) are set
+## automatically
+
+xml=o.save_to_xml 
+XML::MappingError: no value, and no default value, for attribute: reference
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:724:in `obj_to_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:218:in `block in fill_into_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `each'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `fill_into_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:229:in `save_to_xml'
+## can't save as long as there are still unset attributes without
+## default values
+
+o.reference = "FOOBAR-1234"
+
+o.client = Client.new
+o.client.name = 'Ford Prefect'
+o.client.home_address = Address.new
+o.client.home_address.street = '42 Park Av.'
+o.client.home_address.city = 'small planet'
+o.client.home_address.zip = 17263
+o.client.home_address.state = 'Betelgeuse system'
+
+o.items={'XY-42' => Item.new}
+o.items['XY-42'].descr = 'improbability drive'
+o.items['XY-42'].quantity = 3
+o.items['XY-42'].unit_price = 299.95
+
+xml=o.save_to_xml
+xml.write($stdout,2)
+
+<order reference='FOOBAR-1234'>
+  <Client>
+    <Name>
+      Ford Prefect
+    </Name>
+    <Address where='home'>
+      <City>
+        small planet
+      </City>
+      <State>
+        Betelgeuse system
+      </State>
+      <ZIP>
+        17263
+      </ZIP>
+      <Street>
+        42 Park Av.
+      </Street>
+    </Address>
+  </Client>
+  <Item reference='XY-42'>
+    <Description>
+      improbability drive
+    </Description>
+    <Quantity>
+      3
+    </Quantity>
+    <UnitPrice>
+      299.95
+    </UnitPrice>
+  </Item>
+</order>
+## the root element name when saving an object to XML will by default
+## be derived from the class name (in this example, "Order" became
+## "order"). This can be overridden on a per-class basis; see
+## XML::Mapping::ClassMethods#root_element_name for details.
+ +

As shown in the example, you have to include XML::Mapping into a class to turn it into a +“mapping class”. There are no other restrictions imposed on mapping +classes; you can add attributes and methods to them, include additional +modules in them, derive them from other classes, derive other classes from +them etc.pp.

+ +

An instance of a mapping class can be created from/converted into an XML node with methods like XML::Mapping::ClassMethods#load_from_xml, +XML::Mapping#save_to_xml, +XML::Mapping::ClassMethods#load_from_file, +XML::Mapping#save_to_file. +Special class methods like “text_node”, “array_node” etc., called +node factory methods, may be called from the +body of the class definition to define instance attributes that are +automatically and bidirectionally mapped to subtrees of the XML element an instance of the class is mapped to.

+ +

Single-attribute Nodes

+ +

For example, in the definition

+ +
class Address
+  include XML::Mapping
+
+  text_node :city, "City"
+  text_node :state, "State"
+  numeric_node :zip, "ZIP"
+  text_node :street, "Street"
+end
+
+ +

the first call to text_node creates an attribute named “city” which is +mapped to the text of the XML child element defined +by the XPath expression “City” (xml-mapping includes an XPath interpreter +that can also be used seperately; see below). When +you create an instance of Address from an XML element (using Address.load_from_file(file_name) or +Address.load_from_xml(rexml_element)), that instance's “city” attribute +will be set to the text of the XML element's +“City” child element. When you convert an instance of Address +into an XML element, a sub-element “City” is added +and its text is set to the current value of the city +attribute. The other node types (numeric_node, array_node etc.) work +analogously. Generally said, when an instance of the above +Address class is created from or converted to an XML tree, each of the four nodes in the class maps some +parts of that XML tree to a single, specific +attribute of the Adress instance. The name of that attribute +is given in the first argument to the node factory method. Such a node is +called a “single-attribute node”. All node types that come with xml-mapping +except one (choice_node, which I'll talk about below) are +single-attribute nodes.

+ +

Default Values

+ +

For each single-attribute node you may define a default value +which will be set if there was no value defined for the attribute in the XML source.

+ +

From the example:

+ +
class Signature
+  include XML::Mapping
+
+  text_node :position, "Position", :default_value=>"Some Employee"
+end
+
+ +

The semantics of default values are as follows:

+
  • +

    when creating a new instance from scratch:

    +
    • +

      attributes with default values are set to their default values

      +
    • +

      attributes without default values are left unset

      +
    +
+ +

(when defining your own initializer, you'll have to call the inherited +initialize method in order to get this behaviour)

+
  • +

    when loading an instance from an XML document:

    +
    • +

      attributes without default values that are not represented in the XML raise an error

      +
    • +

      attributes with default values that are not represented in the XML are set to their default values

      +
    • +

      all other attributes are set to their respective values as present in the +XML

      +
    +
  • +

    when saving an instance to an XML document:

    +
    • +

      unset attributes without default values raise an error

      +
    • +

      attributes with default values that are set to their default values are +not saved

      +
    • +

      all other attributes are saved

      +
    +
+ +

This implies that:

+
  • +

    attributes that are set to their respective default values are not +represented in the XML

    +
  • +

    attributes without default values must be set explicitly before saving

    +
+ +

Single-attribute Nodes with Sub-objects

+ +

Single-attribute nodes of type array_node, +hash_node, and object_node recursively map one or +more subtrees of their XML to sub-objects (e.g. +array elements or hash values) of their attribute. For example, with the +line

+ +
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
+
+ +

, an attribute named “signatures” is added to the surrounding class (here: +Order); the attribute will be an array whose elements +correspond to the XML sub-trees yielded by the XPath +expression “Signed-By/Signature” (relative to the tree corresponding to the +Order instance). Each element will be of class +Signature (internally, each element is created from its +corresponding XML subtree by just calling +Signature.load_from_xml(the_subtree)). The reason why the path +“Signed-By/Signature” is provided in two arguments instead of just one +combined one becomes apparent when marshalling the array (along with the +surrounding Order object) back into a sequence of XML elements. When that happens, “Signed-By” names the +common base element for all those elements, and “Signature” is the path +that will be duplicated for each element. For example, when the +signatures attribute contains an array with 3 +Signature instances (let's call them sig1, +sig2, and sig3) in it, it will be marshalled to +an XML tree that looks like this:

+ +
<Signed-By>
+  <Signature>
+    [marshalled object sig1]
+  </Signature>
+  <Signature>
+    [marshalled object sig2]
+  </Signature>
+  <Signature>
+    [marshalled object sig3]
+  </Signature>
+</Signed-By>
+ +

Internally, each Signature instance is stored into its +<Signature> sub-element by calling +the_signature_instance.fill_into_xml(the_sub_element). The +input document in the example above shows how this ends up looking.

+ +

hash_nodes work similarly, but they define hash-valued +attributes instead of array-valued ones.

+ +

object_nodes are the simplest of the three types of +single-attribute nodes with sub-objects. They just map a single given +subtree directly to their attribute value. See the example for examples :)

+ +

The mentioned methods load_from_xml and +fill_into_xml are the only methods classes must implement in +order to be usable in the :class=> keyword arguments to +node factory methods. Mapping classes (i.e. classes that include +XML::Mapping) automatically inherit those functions and can thus be +readily used in :class=> arguments, as shown for the +Signature class in the array_node call above. In +addition to that, xml-mapping adds those methods to some of Ruby's core +classes, namely String and Numeric (and thus +Float, Integer, and BigInt). So you +can also use strings or numbers as sub-objects of attributes of +array_node, hash_node, or +object_node nodes. For example, say you have an XML document like this one:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<people>
+  <names>
+    <name>Jim</name>
+    <name>Susan</name>
+    <name>Herbie</name>
+    <name>Nancy</name>
+  </names>
+</people>
+ +

, and you want to map all the names to a string array attribute +names, you could do it like this:

+ +
require 'xml/mapping'
+class People
+  include XML::Mapping
+  array_node :names, "names", "name", :class=>String
+end
+
+ +

usage:

+ +
ppl=People.load_from_file("stringarray.xml") 
+=> #<People:0x007ff64a0cda08 @names=["Jim", "Susan", "Herbie", "Nancy"]>
+ppl.names 
+=> ["Jim", "Susan", "Herbie", "Nancy"]
+
+ppl.names.concat ["Mary","Arnold"] 
+=> ["Jim", "Susan", "Herbie", "Nancy", "Mary", "Arnold"]
+ppl.save_to_xml.write $stdout,2
+
+<people>
+  <names>
+    <name>
+      Jim
+    </name>
+    <name>
+      Susan
+    </name>
+    <name>
+      Herbie
+    </name>
+    <name>
+      Nancy
+    </name>
+    <name>
+      Mary
+    </name>
+    <name>
+      Arnold
+    </name>
+  </names>
+</people>
+ +

As a side node, this feature actually makes text_node and +numeric_node special cases of object_node. For +example, text_node :attr, "path" is the same as +object_node :attr, "path", :class=>String.

+ +

Polymorphic Sub-objects, Marshallers/Unmarshallers

+ +

Besides the :class keyword argument, there are alternative +ways for a single-attribute node with sub-objects to specify the way the +sub-objects are created from/marshalled into their subtrees.

+ +

First, it's possible not to specify anything at all – in that case, the +class of a sub-object will be automatically deduced from the root element +name of its subtree. This allows you to achieve a kind of “polymorphic”, +late-bound way to decide about the sub-object's class. The following +example document contains a hierarchical, recursive set of named +“documents” and “folders”, where folders hold a set of entries, each of +which may again be either a document or a folder:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<folder name="home">
+  <document name="plan">
+    <contents> inhale, exhale</contents>
+  </document>
+
+  <folder name="work">
+    <folder name="xml-mapping">
+      <document name="README">
+        <contents>foo bar baz</contents>
+      </document>
+    </folder>
+  </folder>
+
+</folder>
+ +

This can be mapped to Ruby like this:

+ +
require 'xml/mapping'
+
+class Entry
+  include XML::Mapping
+
+  text_node :name, "@name"
+end
+
+
+class Document <Entry
+  include XML::Mapping
+
+  text_node :contents, "contents"
+end
+
+
+class Folder <Entry
+  include XML::Mapping
+
+  array_node :entries, "document|folder", :default_value=>[]
+
+  def [](name)
+    entries.select{|e|e.name==name}[0]
+  end
+
+  def append(name,entry)
+    entries << entry
+    entry.name = name
+    entry
+  end
+end
+
+ +

Usage:

+ +
root = XML::Mapping.load_object_from_file "documents_folders.xml" 
+=> #<Folder:0x007ff6499c0f58 @entries=[#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">], @name="home">
+root.name 
+=> "home"
+root.entries 
+=> [#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">]
+
+root.append "etc", Folder.new
+root["etc"].append "passwd", Document.new
+root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh"
+root["etc"].append "hosts", Document.new
+root["etc"]["hosts"].contents = "127.0.0.1 localhost"
+
+xml = root.save_to_xml 
+=> <folder name='home'> ... </>
+xml.write $stdout,2
+
+<folder name='home'>
+  <document name='plan'>
+    <contents>
+       inhale, exhale
+    </contents>
+  </document>
+  <folder name='work'>
+    <folder name='xml-mapping'>
+      <document name='README'>
+        <contents>
+          foo bar baz
+        </contents>
+      </document>
+    </folder>
+  </folder>
+  <folder name='etc'>
+    <document name='passwd'>
+      <contents>
+        foo:x:2:2:/bin/sh
+      </contents>
+    </document>
+    <document name='hosts'>
+      <contents>
+        127.0.0.1 localhost
+      </contents>
+    </document>
+  </folder>
+</folder>
+ +

As you see, the Folder#entries attribute is mapped via an +array_node that does not specify a :class or anything else to +govern the instantiation of the array's elements. This causes +xml-mapping to deduce the class of each array element from the root element +name of the corresponding XML tree. In this example, +the root element name is either “document” or “folder”. The mapping between +root element names and class names is the one briefly described in example at the beginning of this document – the +unqualified class name is just converted to lower case and “dashed”, e.g. +Foo::Bar::MyClass becomes “my-class”; and you may overwrite this on a +per-class basis by calling root_element_name +"the-new-name" in the class body. In our example, the root +element name “document” leads to an instantiation of class +Document, and the root element name “folder” leads to an +instantiation of class Folder.

+ +

Incidentally, the last example shows that you can readily derive mapping +classes from one another (as said before, you can also derive mapping +classes from other classes, include other modules into them etc. at will). +This works just like intuition thinks it should – when deriving one mapping +class from another one, the list of nodes in effect when loading/saving +instances of the derived class will consist of all nodes of that class and +all superclasses, starting with the topmost superclass that has nodes +defined. There is one thing to take care of though: When deriving mapping +classes from one another, you have to make sure to include +XML::Mapping in each class. This requirement exists purely due to +ease-of-implementation considerations; there are probably ways to do away +with it, but the inconvenience seemed not severe enough for me to bother +(as yet). Still, you might get “strange” errors if you forget to do it for +a class.

+ +

Besides the :class keyword argument and no argument, there is +a third way to specify the way the sub-objects are created from/marshalled +into their subtrees: :marshaller and/or +:unmarshaller keyword arguments. Here you pass procs in which +you just do all the work manually. So this is basically a “catch-all” for +cases where the other two alternatives are not appropriate for the problem +at hand. (TODO: Use other example?) Let's say we want to +extend the Signature class from the initial example to include +the date on which the signature was created. We want the new XML representation of such a signature to look like +this:

+ +
<Signature>
+  <Name>John Doe</Name>
+  <Position>product manager</Position>
+  <signed-on>
+    <day>13</day>
+    <month>2</month>
+    <year>2005</year>
+  </signed-on>
+</Signature>
+ +

So, a new “signed-on” element was added that holds the day, month, and +year. In the Signature instance in Ruby, we want the date to +be stored in an attribute named signed_on of type +Time (that's Ruby's built-in Time class).

+ +

One could think of using object_node, but something like +object_node :signed_on, "signed-on", :class=>Time +won't work because Time isn't a mapping class and +doesn't define methods load_from_xml and +fill_into_xml (we could easily define those though; we'll +talk about that possibility here and here). The fastest, most ad-hoc way to +achieve what we want are :marshaller and :unmarshaller keyword arguments, +like this:

+ +
require 'xml/mapping'
+require 'xml/xxpath_methods'
+
+class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+  object_node :signed_on, "signed-on",
+              :unmarshaller=>proc{|xml|
+                               y,m,d = [xml.first_xpath("year").text.to_i,
+                                        xml.first_xpath("month").text.to_i,
+                                        xml.first_xpath("day").text.to_i]
+                               Time.local(y,m,d)
+                             },
+              :marshaller=>proc{|xml,value|
+                             e = xml.elements.add; e.name = "year"; e.text = value.year
+                             e = xml.elements.add; e.name = "month"; e.text = value.month
+                             e = xml.elements.add; e.name = "day"; e.text = value.day
+
+                             # xml.first("year",:ensure_created=>true).text = value.year
+                             # xml.first("month",:ensure_created=>true).text = value.month
+                             # xml.first("day",:ensure_created=>true).text = value.day
+                           }
+end
+
+ +

The :unmarshaller proc will be called whenever a +Signature instance is being read in from an XML source. The xml argument passed to the +proc contains (as a REXML::Element +instance) the XML subtree corresponding to the +node's attribute's sub-object currently being read. In the case of +our object_node, the sub-object is just the node's +attribute (signed_on) itself, and the subtree is the one +rooted at the <signed-on> element (if this were e.g. an +array_node, the :unmarshaller proc would be +called once for each array element, and xml would hold the +subtree corresponding to the “current” array element). The proc is expected +to extract the sub-object's data from xml and return the +sub-object. So we have to read the “year”, “month”, and “day” elements, +construct a Time instance from them and return that. One could +just use the REXML API to do that, but I've +decided here to use the XPath interpreter that comes with xml-mapping +(xml/xxpath), and specifically the 'xml/xxpath_methods' utility +library that adds methods like first to REMXML::Element. We +call first on xml three times, passing XPath +expressions to extract the “year”/“month”/“day” sub-elements, construct the +Time instance from that and return it. The XPath library is +explained in more detail below.

+ +

The :marshaller proc will be called whenever a +Signature instance is being written into an XML tree. xml is again the XML subtree rooted at the <signed-on> element (it +will still be empty when this proc is called), and value is +the current value of the sub-object (again, since this is an +object_node, value is the node's attribute, +i.e. the Time instance). We have to fill xml with +the data from value here. So we add three elements “year”, +“month” and “day” and set their texts to the corresponding values from +value. The commented-out code shows an alternative +implementation of the same thing using the XPath interpreter.

+ +

It should be mentioned again that :marshaller/:unmarshaller procs are +possible with all single-attribute nodes with sub-objects, i.e. with +object_node, array_node, and +hash_node. So, if you wanted to map a whole array of date +values, you could use array_node with the same +:marshaller/:unmarshaller procs as above, for example:

+ +
array_node :birthdays, "birthdays", "birthday",
+           :unmarshaller=> <as above>,
+           :marshaller=> <as above>
+ +

You can see that :marshaller/:unmarshaller procs give you more flexibility, +but they also impose more work because you essentially have to do all the +work of marshalling/unmarshalling the sub-objects yourself. If you find +yourself copying and pasting marshaller/unmarshaller procs all over the +place, you should instead define your own node type or mix the +marshalling/unmarshalling capabilities into the Time class +itself. This is explained here and here, and you'll see that it's not +really much more work than writing :marshaller and :unmarshaller procs (you +essentially just move the code from those procs into your own node type +resp. into the Time class), so you should not hesitate to do +this.

+ +

Another thing worth mentioning is that you don't have to specify +both a :marshaller and an :unmarshaller simultaneously. You can as +well give only one of them, and in addition to that pass a +:class argument or no argument. When you do that, the +specified marshaller (or unmarshaller) will be used when marshalling (resp. +unmarshalling) the sub-objects, and the other passed argument +(:class or none) will be employed when unmarshalling (resp. +marshalling) the sub-objects. So, in effect, you can deactivate or +“short-cut” some part of the marshalling/unmarshalling functionality of a +node type while retaining another part.

+ +

Attribute Handling Details, Augmenting Existing Classes

+ +

I'll shed some more light on how single-attribute nodes add mapped +attributes to Ruby classes. An attribute declaration like

+ +
text_node :city, "City"
+
+ +

maps some portion of the XML tree (here: the “City” +sub-element) to an attribute (here: “city”) of the class whose body the +declaration appears in. When writing (marshalling) instances of the +surrounding class into an XML document, xml-mapping +will read the attribute value from the instance using the function named +city; when reading (unmarshalling) an instance from an XML document, xml-mapping will use the one-parameter +function city= to set the attribute in the instance to the +value read from the XML document.

+ +

If these functions don't exist at the time the node declaration is +executed, xml-mapping adds default implementations that simply read/write +the attribute value to instance variables that have the same name as the +attribute. For example, the city attribute declaration in the +Address class in the example added functions city +and city= that read/write from/to the instance variable +@city.

+ +

If, however, these functions already exist prior to defining the +attributes, xml-mapping will leave them untouched, so your precious +self-written accessor methods that do whatever complicated internal +processing of the data won't be overwritten.

+ +

This means that you can not only create new mapping classes from scratch, +you can also take existing classes that contain some “business logic” and +“augment” them with xml-mapping capabilities. As a simple example, +let's augment Ruby's “Time” class with node declarations that +declare XML mappings for the day, month etc. fields:

+ +
class Time
+  include XML::Mapping
+
+  numeric_node :year, "year"
+  numeric_node :month, "month"
+  numeric_node :day, "mday"
+  numeric_node :hour, "hours"
+  numeric_node :min, "minutes"
+  numeric_node :sec, "seconds"
+end
+
+
+nowxml=Time.now.save_to_xml 
+=> <time> ... </>
+nowxml.write($stdout,2)
+<time>
+  <year>
+    2015
+  </year>
+  <month>
+    3
+  </month>
+  <mday>
+    1
+  </mday>
+  <hours>
+    15
+  </hours>
+  <minutes>
+    31
+  </minutes>
+  <seconds>
+    6
+  </seconds>
+</time>
+ +

Here XML mappings are defined for the existing +fields year, month etc. Xml-mapping noticed that +the getter methods for those attributes existed, so it didn't overwrite +them. When calling save_to_xml on a Time object, +these methods are called and return the object's values for those +fields, which then get written to the output XML.

+ +

So you can convert Time objects into XML trees. What about reading them back in from XML? All XML reading operations +go through <Class>.load_from_xml. The +load_from_xml class method inherited from XML::Mapping (see XML::Mapping::ClassMethods#load_from_xml) +allocates a new instance of the class (Time), then calls +fill_from_xml (i.e. XML::Mapping#fill_from_xml) +on it. fill_from_xml iterates over all our nodes in the order +of their definition. For each node, its data (the <year>, or +<month>, or <day> etc. element) is read from the XML source and then written to the Time +instance via the respective setter method (year=, +month=, day= etc.). These methods didn't +exist in Time before (Time objects are +immutable), so xml-mapping defined its own, default setter methods that +just set @year, @month etc. This is of course +pretty useless because Time objects don't hold their time +in these variables, so the setter methods don't really change the time +of the Time object. So we have to redefine +load_from_xml for the Time class:

+ +
def Time.load_from_xml(xml, options={:mapping=>:_default})
+  year,month,day,hour,min,sec =
+    [xml.first_xpath("year").text.to_i,
+     xml.first_xpath("month").text.to_i,
+     xml.first_xpath("mday").text.to_i,
+     xml.first_xpath("hours").text.to_i,
+     xml.first_xpath("minutes").text.to_i,
+     xml.first_xpath("seconds").text.to_i]
+  Time.local(year,month,day,hour,min,sec)
+end
+
+ +

Other Nodes

+ +

All nodes I've shown so far (node types text_node, numeric_node, +boolean_node, object_node, array_node, and hash_node) were single-attribute +nodes: The first parameter to the node factory method of such a node is an +attribute name, and the attribute of that name is the only piece of the +state of instances of the node's mapping class that gets read/written +by the node.

+ +

choice_node

+ +

There is one node type distributed with xml-mapping that is not a +single-attribute node: choice_node. A choice_node +allows you to specify a sequence of pairs, each consisting of an XPath +expression and another node (any node is supported here, including other +choice_nodes). When reading in an XML source, the +choice_node will delegate the work to the first node in the sequence whose +corresponding XPath expression was matched in the XML. When writing an object back to XML, the choice_node will delegate the work to the +first node whose data was “present” in the object (for single-attribute +nodes, the data is considered “present” if the node's attribute is +non-nil; for choice_nodes, the data is considered “present” if at least one +of the node's sub-nodes is “present”).

+ +

As a (somewhat contrived) example, here's a mapping for +Publication objects that have either a single author +(contained in an “author” XML attribute) or several +“contributors” (contained in a sequence of “contr” XML elements):

+ +
class Publication
+  include XML::Mapping
+
+  choice_node :if,    '@author', :then, (text_node :author, '@author'),
+              :elsif, 'contr',   :then, (array_node :contributors, 'contr', :class=>String)
+end
+
+### usage
+
+p1 = Publication.load_from_xml(REXML::Document.new('<publication author="Jim"/>').root)
+=> #<Publication:0x007ff64a166a78 @author="Jim">
+
+p2 = Publication.load_from_xml(REXML::Document.new('
+<publication>
+  <contr>Chris</contr>
+  <contr>Mel</contr>
+  <contr>Toby</contr>
+</publication>').root)
+=> #<Publication:0x007ff64a155f48 @contributors=["Chris", "Mel", "Toby"]>
+ +

The symbols :if, :then, and :elsif (but not :else – see below) in the +choice_node's node factory method call are ignored; they +may be sprinkled across the argument list at will (preferably the way shown +above of course) to increase readability.

+ +

The rest of the arguments specify the mentioned sequence of XPath +expressions and corresponding nodes.

+ +

When reading a Publication object from XML, the XPath expressions from the +choice_node (@author and contr) will +be matched in sequence against the source XML tree +until a match is found or the end of the argument list is reached. If the +end is reached, an exception is raised. Otherwise, for the first XPath +expression that matched, the corresponding node will be invoked (i.e. used +to read actual data from the XML source into the +Person object). If you specify :else, :default, or :otherwise +in place of an XPath expression, this is treated as an XPath expression +that always matches. So you can use :else (or :default or :otherwise) for a +“fallback” node that will be used if none of the other XPath expressions +matched (an example for this follows).

+ +

When writing a Publication object back to XML, the first node in the sequence whose data is +“present” in the source object will be invoked to write data from the +object into the target XML tree (and the +corresponding XPath expression will be created in the XML tree if it doesn't exist already). If there is +no such node in the sequence, an exception is raised. As said above, for +single-attribute nodes, the node's data is considered “present” if the +node's attribute is non-nil. So, if you write a +Publication object to XML, and either +the author or the contributors attribute of the +object is set, it will be written; if both attributes are nil, an exception +will be raised.

+ +

A frequent use case for choice_nodes will probably be object attributes +that may be represented in multiple alternative ways in XML. As an example, consider “Person” objects where the +name of the person should be stored alternatively in a sub-element named +name, or an attribute named name, or in the text +of the person element itself. You can achieve this with +choice_node like this:

+ +
class Person
+  include XML::Mapping
+
+  choice_node :if,    'name',  :then, (text_node :name, 'name'),
+              :elsif, '@name', :then, (text_node :name, '@name'),
+              :else,  (text_node :name, '.')
+end
+
+### usage
+
+p1 = Person.load_from_xml(REXML::Document.new('<person name="Jim"/>').root)
+=> #<Person:0x007ff64a1cd660 @name="Jim">
+
+p2 = Person.load_from_xml(REXML::Document.new('<person><name>James</name></person>').root)
+=> #<Person:0x007ff64a1c54b0 @name="James">
+
+p3 = Person.load_from_xml(REXML::Document.new('<person>Suzy</person>').root)
+=> #<Person:0x007ff64a1b6820 @name="Suzy">
+
+
+p1.save_to_xml.write($stdout)
+<person><name>Jim</name></person>
+p2.save_to_xml.write($stdout)
+<person><name>James</name></person>
+p3.save_to_xml.write($stdout)
+<person><name>Suzy</name></person>
+ +

Here all sub-nodes of the choice_nodes are single-attribute nodes +(text_nodes) with the same attribute (name). As you see, when +writing persons to XML, the name is always stored in +a <name> sub-element. Of course, this is because that alternative +appears first in the choice_node.

+ +

Readers/Writers

+ +

Finally, all nodes support keyword arguments :reader and :writer +which allow you to extend or completely override the reading and/or writing +functionality of the node with your own code. The :reader as well as the +:writer argument must be a proc that takes as its arguments the Ruby object +to be read/written (instance of the mapping class the node belongs to) and +the XML tree to be written to/read from. An optional +third argument may be specified – it will receive a proc that wraps the +default reader/writer functionality of the node.

+ +

The :reader proc is for reading (from the XML into +the object), the :writer proc is for writing (from the object into the XML).

+ +

Here's a (really contrived) example:

+ +
class Foo
+  include XML::Mapping
+
+  text_node :name, "@name", :reader=>proc{|obj,xml,default_reader|
+                                       default_reader.call(obj,xml)
+                                       obj.name += xml.attributes['more']
+                                     },
+                            :writer=>proc{|obj,xml|
+                                       xml.attributes['bar'] = "hi #{obj.name} ho"
+                                     }
+end
+
+f = Foo.load_from_xml(REXML::Document.new('<foo name="Jim" more="XYZ"/>').root)
+=> #<Foo:0x007ff64a10e8c8 @name="JimXYZ">
+
+xml = f.save_to_xml 
+xml.write $stdout,2 
+<foo bar='hi JimXYZ ho'/>
+ +

So there's a “Foo” class with a text_node that would by default +(without the :reader and :writer proc) map the Ruby attribute “name” to the +XML attribute “name”. The :reader proc is invoked +when reading from XML into a Foo +object. The xml argument is the XML +tree, obj is the object. default_reader is the +proc that wraps the default reading functionality of the node. We invoke it +at the beginning. For this text_node, the default reading functionality is +to take the text of the “name” attribute of xml and put it +into the name attribute of obj. After that, we +take the text of the “more” attribute of xml and append it to +the name attribute of obj. So the XML tree <foo name="Jim" +more="XYZ"/> is converted to a Foo object +with name=“JimXYZ”.

+ +

In our :writer proc, we only take obj (the Foo +object to be written to XML) and xml +(the XML tree the stuff is to be written to). +Analogously to the :reader, we could take a proc that wraps the default +writing functionality of the node, but we don't do that here–we +completely override the writing functionality with our own code, which just +takes the name attribute of the object and writes “hi <the +name> ho” to a bar XML attribute in +the XML tree (stupid example, I know).

+ +

As a special convention, if you specify both a :reader and a :writer for a +node, and in both cases you do /not/ call the default behaviour, then you +should use the generic node type node, e.g.:

+ +
class SomeClass
+  include XML::Mapping
+
+  ....
+
+  node :reader=>proc{|obj,xml| ...},
+       :writer=>proc{|obj,xml| ...}
+end
+ +

(since you're completely replacing both the reading and the writing +functionality, you're effectively replacing all the functionality of +the node, so it would be pointless and confusing to use one of the more +“specific” node types)

+ +

As you see, the purpose of readers and writers is to make it possible to +augment or override a node's functionality arbitrarily, so there +shouldn't be anything that's absolutely impossible to achieve with +xml-mapping. However, if you use readers and writers without invoking the +default behaviour, you really do everything manually, so you're not +doing any less work than you would do if you weren't using xml-mapping +at all. So you'll probably use readers and/or writers for those bits of +your mapping semantics that can't be achieved with xml-mapping's +predefined node types (an alternative approach might be to override the +post_load and/or post_save instance methods on +the mapping class – see the reference documentation).

+ +

An advice similar to the one given above for marshallers/unmarshallers +applies here as well: If you find yourself writing lots of readers and +writers that only differ in some easily parameterizable aspects, you should +think about defining your own node types. We talk about that below, and it generally just means that you +move the (sensibly parameterized) code from your readers/writers to your +node types.

+ +

Multiple Mappings per Class

+ +

Sometimes you might want to represent the same Ruby object in multiple +alternative ways in XML. For example, the name of a +“Person” object could be represented either in a “name” element or a “name” +attribute.

+ +

xml-mapping supports this by allowing you to define multiple disjoint +“mappings” for a mapping class. A mapping is by convention identified with +a symbol, e.g. :my_mapping, :other_mapping etc., +and each mapping comprises a root element name and a set of node +definitions. In the body of a mapping class definition, you switch to +another mapping with use_mapping :the_mapping. All following +node declarations will be added to that mapping unless you specify +the option :mapping=>:another_mapping for a node declaration (all node +types support that option). The default mapping (the mapping used if there +was no previous use_mapping in the class body) is named +:_default.

+ +

All the worker methods like load_from_xml/file, +save_to_xml/file, load_object_from_xml/file +support a :mapping keyword argument to specify the mapping, +which again defaults to :_default.

+ +

In the following example, we define two mappings (the default one and a +mapping named :other) for Person objects with a +name, an age and an address:

+ +
require 'xml/mapping'
+
+class Address; end
+
+class Person
+  include XML::Mapping
+
+  # the default mapping. Stores the name and age in XML attributes,
+  # and the address in a sub-element "address".
+
+  text_node :name, "@name"
+  numeric_node :age, "@age"
+  object_node :address, "address", :class=>Address
+
+  use_mapping :other
+
+  # the ":other" mapping. Non-default root element name; name and age
+  # stored in XML elements; address stored in the person's element
+  # itself
+
+  root_element_name "individual"
+  text_node :name, "name"
+  numeric_node :age, "age"
+  object_node :address, ".", :class=>Address
+
+  # you could also specify the mapping on a per-node basis with the
+  # :mapping option, e.g.:
+  #
+  # numeric_node :age, "age", :mapping=>:other
+end
+
+
+class Address
+  include XML::Mapping
+
+  # the default mapping.
+
+  text_node :street, "street"
+  numeric_node :number, "number"
+  text_node :city, "city"
+  numeric_node :zip, "zip"
+
+  use_mapping :other
+
+  # the ":other" mapping.
+
+  text_node :street, "street-name"
+  numeric_node :number, "street-name/@number"
+  text_node :city, "city-name"
+  numeric_node :zip, "city-name/@zip-code"
+end
+
+
+### usage
+
+## XML representation of a person in the default mapping
+xml = REXML::Document.new('
+<person name="Suzy" age="28">
+  <address>
+    <street>Abbey Road</street>
+    <number>72</number>
+    <city>London</city>
+    <zip>18827</zip>
+  </address>
+</person>').root
+
+## load using the default mapping
+p = Person.load_from_xml xml 
+=> #<Person:0x007ff64a23e9c8 @name="Suzy", @age=28, @address=#<Address:0x007ff64a23d4b0 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
+
+## save using the default mapping
+xml2 = p.save_to_xml
+xml2.write $stdout,2 
+<person name='Suzy' age='28'>
+  <address>
+    <street>
+      Abbey Road
+    </street>
+    <number>
+      72
+    </number>
+    <city>
+      London
+    </city>
+    <zip>
+      18827
+    </zip>
+  </address>
+</person>
+## xml2 identical to xml
+
+
+## now, save the same person to XML using the :other mapping...
+other_xml = p.save_to_xml :mapping=>:other
+other_xml.write $stdout,2 
+<individual>
+  <name>
+    Suzy
+  </name>
+  <age>
+    28
+  </age>
+  <street-name number='72'>
+    Abbey Road
+  </street-name>
+  <city-name zip-code='18827'>
+    London
+  </city-name>
+</individual>
+## load it again using the :other mapping
+p2 = Person.load_from_xml other_xml, :mapping=>:other 
+=> #<Person:0x007ff64a20c838 @name="Suzy", @age=28, @address=#<Address:0x007ff64a2079a0 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
+
+## p2 identical to p
+ +

In this example, each of the two mappings contains nodes that map the same +set of Ruby attributes (name, age and address). This is probably what you +want most of the time (since you're normally defining multiple XML mappings for the same Ruby data), but it's not +a necessity at all. When a mapping class is defined, xml-mapping will add +all Ruby attributes from all mappings to it.

+ +

You may have noticed that the object_nodes in the +Person class apply the mapping they were themselves defined in +to their sub-ordinated class (Address). This is the case for +all Single-attribute Nodes with Sub-objects +(object_node, array_node and +hash_node) unless you explicitly specify a different mapping +for the sub-object(s) using the option :sub_mapping, e.g.

+ +
object_node :address, "address", :class=>Address, :sub_mapping=>:other
+
+ +

Defining your own Node Types

+ +

It's easy to write additional node types and register them with the +xml-mapping library (the following node types come with xml-mapping: +node, text_node, numeric_node, +boolean_node, object_node, +array_node, hash_node, choice_node).

+ +

I'll first show an example, then some more theoretical insight.

+ +

Example

+ +

Let's say we want to extend the Signature class from the +example to include the time at which the signature was created. We want the +new XML representation of such a signature to look +like this:

+ +
<Signature>
+  <Name>John Doe</Name>
+  <Position>product manager</Position>
+  <signed-on>
+    <day>13</day>
+    <month>2</month>
+    <year>2005</year>
+  </signed-on>
+</Signature>
+ +

(we only save year, month and day to make this example shorter), and the +mapping class declaration to look like this:

+ +
class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+  time_node :signed_on, "signed-on", :default_value=>Time.now
+end
+
+ +

(i.e. a new “time_node” declaration was added).

+ +

We want this time_node call to define an attribute named +signed_on which holds the date value from the XML document in an instance of class Time.

+ +

This node type can be defined with this piece of code:

+ +
require 'xml/mapping/base'
+
+class TimeNode < XML::Mapping::SingleAttributeNode
+  def initialize(*args)
+    path,*args = super(*args)
+    @y_path = XML::XXPath.new(path+"/year")
+    @m_path = XML::XXPath.new(path+"/month")
+    @d_path = XML::XXPath.new(path+"/day")
+    args
+  end
+
+  def extract_attr_value(xml)
+    y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i,
+                                     @m_path.first(xml).text.to_i,
+                                     @d_path.first(xml).text.to_i]
+                                  }
+    Time.local(y,m,d)
+  end
+
+  def set_attr_value(xml, value)
+    @y_path.first(xml,:ensure_created=>true).text = value.year
+    @m_path.first(xml,:ensure_created=>true).text = value.month
+    @d_path.first(xml,:ensure_created=>true).text = value.day
+  end
+end
+
+
+XML::Mapping.add_node_class TimeNode
+
+ +

The last line registers the new node type with the xml-mapping library. The +name of the node factory method (“time_node”) is automatically derived from +the class name of the node type (“TimeNode”).

+ +

There will be one instance of the node type TimeNode per +time_node declaration per mapping class (not per mapping class +instance). That instance (the “node” for short) will be created by the node +factory method (time_node); there's no need to instantiate +the node type directly. The time_node method places the node +into the mapping class; the @owner attribute of the node is set to +reference the mapping class. The node factory method passes the mapping +class the node appears in (Signature), followed by its own +arguments, to the node's constructor. In the example, the +time_node method calls TimeNode.new(Signature, +:signed_on, "signed-on", :default_value=>Time.now)). +new of course creates the node and then delegates the +arguments to our initializer initialize. We first call the +superclass's initializer, which strips off from the argument list those +arguments it handles itself, and returns the remaining ones. In this case, +the superclass XML::Mapping::SingleAttributeNode +handles the Signature, :signed_on and +:default_value=>Time.now arguments – Signature +is stored into @owner, :signed_on is stored into +@attrname, and {:default_value=>Time.now} is +stored into @options. The remaining argument list +["signed-on"] is returned; we capture the +"signed-on" string in path (the rest of the +argument list (an empty array) we capture in args for returning it +at the end of the initializer. This isn't strictly necessary, it's +just a convention that a node class initializer should always return those +arguments it didn't handle itself). We'll interpret path +as an XPath expression that locates the time value relative to the parent +mapping object's XML tree (in this case, this +would be the XML tree rooted at the +<Signature> element, i.e. the tree the +Signature instance was read from). We'll later have to +read/store the year, month, and day values from +path+"/year", path+"/month", +and path+"/day", respectively, so we create (and +precompile) three corresponding XPath expressions using XML::XXPath.new and store them into +member variables of the node. XML::XXPath is +an XPath implementation that is bundled with xml-mapping. It is very +incomplete, but it supports writing (not just reading) of XML nodes, which is needed to support writing data back +to XML. The XML::XXPath library is explained in more detail +below.

+ +

The extract_attr_value method is called whenever an instance +of the mapping class the node belongs to (Signature in the +example) is being created from an XML tree. The +parameter xml is that tree (again, this is the tree rooted at the +<Signature> element in this example). The method +implementation is expected to extract the single attribute's value from +xml and return it, or raise XML::Mapping::SingleAttributeNode::NoAttrValueSet +if the attribute was “unset” in the XML (this +exception tells the framework that the default value should be put in place +if it was defined), or raise any other exception to signal an error and +abort the whole process. Our superclass XML::Mapping::SingleAttributeNode +will store the returned single attribute's value into the +signed_on attribute of the Signature instance +being read in. In our implementation, we apply the xpath expressions +created during initialization to xml (e.g. +@y_path.first(xml)). An expression +xpath_expr.first(xml) returns (as a REXML element) the first sub-element of xml +that matches xpath_expr, or raises XML::XXPathError if there was no such +element. We apply REXML's text method to the returned element +to get out the element's text, convert it to integer, and supply it to +the constructor of the Time object to be returned. As a side +note, if an XPath expression matches XML attributes, +XML::XXPath methods like first will +return XML::XXPath::Accessors::Attribute +nodes that behave similarly to REXML::Element nodes, including support for +messages like name and text, so this would've worked +also if our XPath expressions had referred to XML +attributes, not elements. The default_when_xpath_err thing +calls the supplied block and returns its value, but maps the exception XML::XXPathError to the mentioned XML::Mapping::SingleAttributeNode::NoAttrValueSet +(any other exceptions fall through unchanged). As said above, XML::Mapping::SingleAttributeNode::NoAttrValueSet +is caught by the framework (more precisely, by our superclass XML::Mapping::SingleAttributeNode), +and the default value is set if it was provided. So you should just wrap +default_when_xpath_err around any applications of XPath +expressions whose non-presence in the XML you want +to be considered a non-presence of the attribute you're trying to +extract. (XML::XXPath is designed to know knothing about XML::Mapping, so it doesn't raise XML::Mapping::SingleAttributeNode::NoAttrValueSet +directly)

+ +

The set_attr_value method is called whenever an instance of +the mapping class the node belongs to (Signature in the +example) is being stored into an XML tree. The +xml parameter is the XML tree (a REXML element node; here this is again the tree +rooted at the <Signature> element); value is +the current value of the single attribute (in this example, the +signed_on attribute of the Signature instance +being stored). xml will most probably be “half-populated” by the +time this method is called – the framework calls the +set_attr_value methods of all nodes of a mapping class in the +order of their definition, letting each node fill its “bit” into +xml. The method implementation is expected to write value +into (the correct sub-elements of) xml, or raise an exception to +signal an error and abort the whole process. No default value handling is +done here; set_attr_value won't be called at all if the +attribute had been set to its default value. In our implementation we grab +the year, month and day values from value (which must be a +Time), and store it into the sub-elements of xml +identified by XPath expressions @y_path, @m_path +and @d_path, respectively. We do this by calling XML::XXPath#first with an +additional parameter :ensure_created=>true. An expression +xpath_expr.first(xml,:ensure_created=>true) works just +like xpath_expr.first(xml) if xpath_expr was +already present in xml. If it was not, it is created (preferably +at the end of xml's list of sub-nodes), and returned. See below for a more detailed documentation of the XPath +interpreter.

+ +

Element order in created XML documents

+ +

As just said, XML::XXPath, when used to +create new XML nodes, generally appends those nodes +to the end of the list of subnodes of the node the xpath expression was +applied to. All xml-mapping nodes that come with xml-mapping use XML::XXPath when writing data to XML, and therefore also append their data to the XML data written by preceding nodes (the nodes are +invoked in the order of their definition). This means that, generally, your +output data will appear in the XML document in the +same order in which the corresponding xml-mapping node definitions appeared +in the mapping class (unless you used XPath expressions like foo which explicitly dictate a fixed position in the +sequence of XML nodes). For instance, in the +Order class from the example at the beginning of this +document, if we put the :signatures node before the +:items node, the <Signed-By> element will +appear before the sequence of <Item> elements +in the output XML.

+ +

The following is a more systematic overview of the basic node types. The +description is self-contained, so some information from the previous +section will be repeated.

+ +

Node Types Are Ruby Classes

+ +

A node type is implemented as a Ruby class derived from XML::Mapping::Node or one of its +subclasses.

+ +

The following node types (node classes) come with xml-mapping (they all +live in the XML::Mapping namespace, which +I've left out here for brevity):

+ +
Node
+ +-SingleAttributeNode
+ |  +-SubObjectBaseNode
+ |  |  +-ObjectNode
+ |  |  +-ArrayNode
+ |  |  +-HashNode
+ |  +-TextNode
+ |  +-NumericNode
+ |  +-BooleanNode
+ +-ChoiceNode
+ +

XML::Mapping::Node is the base class +for all nodes, XML::Mapping::SingleAttributeNode +is the base class for single-attribute nodes, +and XML::Mapping::SubObjectBaseNode +is the base class for single-attribute nodes +with sub-objects. XML::Mapping::TextNode, XML::Mapping::ArrayNode etc. are of +course the text_node, array_node etc. we've +talked about in this document. When you've written a new node class, +you register it with xml-mapping by calling +XML::Mapping.add_node_class MyNode. When you do that, +xml-mapping automatically defines the node factory method for your class – +the method's name (e.g. my_node) is derived from the +node's class name (e.g. Foo::Bar::MyNode) by stripping all parent +module names, and then converting capital letters to lowercase and +preceding them with an underscore. In fact, this is just how all the +predefined node types are defined – those node types are not “special”; +they're defined in the source file +xml/mapping/standard_nodes.rb and then registered normally in +xml/mapping.rb. The source code of the built-in nodes is not +very long or complicated; you may consider reading it in addition to this +text to gain a better understanding.

+ +

How Node Types Work

+ +

The xml-mapping core “operates” node types as follows:

+ +

Node Initialization

+ +

As said above, when a node class is registered with xml-mapping by calling +XML::Mapping.add_node_class TheNodeClass, xml-mapping +automatically generates the node factory method for that type. The node +factory method will effectively be defined as a class method of the XML::Mapping module, which is why one can call +it from the body of a mapping class definition. The generated method will +create a new instance of the node class (a node for short) by +calling new on the node class. The list of parameters to +new will consist of the mapping class, followed by all +arguments that were passed to the node factory method. For example, +when you have this node declaration:

+ +
class MyMappingClass
+  include XML::Mapping
+
+  my_node :foo, "bar", 42, :hi=>"ho"
+end
+
+ +

, then the node factory method (my_node) calls +MyNode.new(MyMappingClass, :foo, "bar", 42, +:hi=>"ho").

+ +

new of course creates the instance and calls initialize +on it. The initialize implementation will generally store the +parameters into some instance variables for later usage. As a convention, +initialize should always extract from the parameter list those +parameters it processes itself, process them, and return an array +containing the remaining (still unprocessed) parameters. Thus, an +implementation of initialize follows this pattern:

+ +
def initialize(*args)
+  myparam1,myparam2,...,myparamx,*args = super(*args)
+
+  .... process the myparam1,myparam2,...,myparamx ....
+
+  # return still unprocessed args
+  args
+end
+ +

(since the called superclass initializer is written the same way, the +parameter array returned by it will already be stripped of all parameters +that the superclass initializer (or any of its superclasses's +initializers) processed)

+ +

This technique is a simple way to “chain” the initializers of all +superclasses of a node class, starting with the topmost one (Node), so that +each initializer can easily find out and process the parameters it is +responsible for.

+ +

The base node class XML::Mapping::Node +provides an initialize implementation that, among other things +(described below), adds self (i.e. the created node) to the +internal list of nodes held by the mapping class, and sets the @owner +attribute of self to reference the mapping class.

+ +

So, effectively there will be one instance of a node class (a node) per +node definition, and that instance lives in the mapping class the node was +defined in.

+ +

Node Operation during Marshalling and Unmarshalling

+ +

When an instance of a mapping class is created or filled from an XML tree, xml-mapping will call xml_to_obj +on all nodes defined in that mapping class in the mapping the node is defined in, in the order of +their definition. Two parameters will be passed: the mapping class instance +being created/filled, and the XML tree the instance +is being created/filled from. The implementation of xml_to_obj +is expected to read whatever pieces of data it is responsible for from the +XML tree and put it into the appropriate +variables/attributes etc. of the instance.

+ +

When an instance of a mapping class is stored or filled into an XML tree, xml-mapping will call obj_to_xml +on all nodes defined in that mapping class in the mapping the node is defined in, in the order of +their definition, again passing as parameters the mapping class instance +being stored, and the XML tree the instance is being +stored/filled into. The implementation of obj_to_xml is +expected to read whatever pieces of data it is responsible for from the +instance and put it into the appropriate XML +elements/XML attr etc. of the XML tree.

+ +

Basic Node Types Overview

+ +

The following is an overview of how initialization and +marshalling/unmarshalling is implemented in the node base classes (Node, +SingleAttributeNode, and SubObjectBaseNode).

+ +

TODO: summary table: member var name; introduced in class; meaning

+ +

Node

+ +

In initialize, the mapping class and the option arguments are +stripped from the argument list. The mapping class is stored in @owner, the +option arguments are stored (as a hash) in @options (the hash will be empty +if no options were given). The mapping the node +is defined in is determined (:mapping option, last use_mapping +or :_default) and stored in @mapping. The node then stores +itself in the list of nodes of the mapping class belonging to the mapping +(@owner.xml_mapping_nodes(:mapping=>@mapping); see XML::Mapping::ClassMethods#xml_mapping_nodes). +This list is the list of nodes later used when marshalling/unmarshalling an +instance of the mapping class with respect to a given mapping. This means +that node implementors will not normally “see” anything of the mapping +(they don't need to access the @mapping variable) because the +marshalling/unmarshalling methods +(obj_to_xml/xml_to_obj) simply won't be +called if the node's mapping is not the same as the mapping the +marshalling/unmarshalling is happening with.

+ +

Furthermore, if :reader and/or :writer options were given, +xml_to_obj resp. obj_to_xml are transparently +overwritten on the node to delegate to the supplied :reader/:writer procs.

+ +

The marshalling/unmarshalling methods +(obj_to_xml/xml_to_obj) are not implemented in +Node (they just raise an exception).

+ +

SingleAttributeNode

+ +

In initialize, the attribute name is stripped from the argument +list and stored in @attrname, and an attribute of that name is added to the +mapping class the node belongs to.

+ +

During marshalling/unmarshalling of an object to/from XML, single-attribute nodes only read/write a single +piece of the object's state: the single attribute (@attrname) the node +handles. Because of this, the +obj_to_xml/xml_to_obj implementations in +SingleAttributeNode call two new methods introduced by SingleAttributeNode, +which must be overwritten by subclasses:

+ +
extract_attr_value(xml)
+
+set_attr_value(xml, value)
+
+ +

extract_attr_value(xml) is called by xml_to_obj +during unmarshalling. xml is the XML tree +being read. The method must read the attribute's value from +xml and return it. xml_to_obj will set the attribute +to that value.

+ +

set_attr_value(xml, value) is called by +obj_to_xml during marshalling. xml is the XML tree being written, value is the current +value of the attribute. The method must write value into (the +correct sub-elements/attributes) of xml.

+ +

SingleAttributeNode also handles the default value, if it was specified +(via the :default_value option): When writing data to XML, set_attr_value(xml, value) won't +be called if the attribute was set to the default value. When reading data +from XML, the extract_attr_value(xml) +implementation must raise a special exception, XML::Mapping::SingleAttributeNode::NoAttrValueSet, +if it wants to indicate that the data was not present in the XML. SingleAttributeNode will catch this exception and +put the default value, if it was defined, into the attribute.

+ +

SubObjectBaseNode

+ +

The initializer will set up additional member variables @sub_mapping, +@marshaller, and @unmarshaller.

+ +

@sub_mapping contains the mapping to be used when reading/writing the +sub-objects (either specified with :sub_mapping, or, by default, the +mapping the node itself was defined in).

+ +

@marshaller and @unmarshaller contain procs that encapsulate +writing/reading of sub-objects to/from XML, as +specified by the user with :class/:marshaller/:unmarshaller etc. options +(the meaning of those different options was described above). The procs are there to be called from +extract_attr_value or set_attr_value whenever the +need arises.

+ +

XPath Interpreter

+ +

XML::XXPath is an XPath parser. It is used in +xml-mapping node type definitions, but can just as well be utilized +stand-alone (it does not depend on xml-mapping). XML::XXPath is very incomplete and probably will +always be, but it should be reasonably efficient (XPath expressions are +precompiled), and, most importantly, it supports write access, which is +needed for writing objects to XML. For example, if +you create the path /foo/bar[3]/baz[@key='hiho'] in the XML document

+ +
<foo>
+  <bar>
+    <baz key="ab">hello</baz>
+    <baz key="xy">goodbye</baz>
+  </bar>
+</foo>
+ +

, you'll get:

+ +
<foo>
+  <bar>
+    <baz key='ab'>hello</baz>
+    <baz key='xy'>goodbye</baz>
+  </bar>
+  <bar/>
+  <bar>
+    <baz key='hiho'/>
+  </bar>
+</foo>
+ +

XML::XXPath is explained in more detail in +the reference documentation and the user_manual_xxpath file.

+ +

License

+ +

xml-mapping is licensed under the Apache License, version 2.0. See the +LICENSE file for details.

+
+ + + + + diff --git a/js/darkfish.js b/js/darkfish.js new file mode 100644 index 0000000..b789a65 --- /dev/null +++ b/js/darkfish.js @@ -0,0 +1,161 @@ +/** + * + * Darkfish Page Functions + * $Id: darkfish.js 53 2009-01-07 02:52:03Z deveiant $ + * + * Author: Michael Granger + * + */ + +/* Provide console simulation for firebug-less environments */ +if (!("console" in window) || !("firebug" in console)) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", + "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +}; + + +/** + * Unwrap the first element that matches the given @expr@ from the targets and return them. + */ +$.fn.unwrap = function( expr ) { + return this.each( function() { + $(this).parents( expr ).eq( 0 ).after( this ).remove(); + }); +}; + + +function showSource( e ) { + var target = e.target; + var codeSections = $(target). + parents('.method-detail'). + find('.method-source-code'); + + $(target). + parents('.method-detail'). + find('.method-source-code'). + slideToggle(); +}; + +function hookSourceViews() { + $('.method-heading').click( showSource ); +}; + +function hookSearch() { + var input = $('#search-field').eq(0); + var result = $('#search-results').eq(0); + $(result).show(); + + var search_section = $('#search-section').get(0); + $(search_section).show(); + + var search = new Search(search_data, input, result); + + search.renderItem = function(result) { + var li = document.createElement('li'); + var html = ''; + + // TODO add relative path to + + + + + + + + + + +
+

Table of Contents - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper

+ +

Pages

+ + +

Classes and Modules

+ + +

Methods

+ +
+ + + + diff --git a/test/all_tests.rb b/test/all_tests.rb deleted file mode 100644 index d2cd4c9..0000000 --- a/test/all_tests.rb +++ /dev/null @@ -1,9 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'xpath_test' -require 'xml_mapping_test' -require 'xml_mapping_adv_test' -require 'multiple_mappings_test' -require 'xxpath_methods_test' -require 'examples_test' -require 'inheritance_test' diff --git a/test/benchmark_fixtures.rb b/test/benchmark_fixtures.rb deleted file mode 100644 index 6a31787..0000000 --- a/test/benchmark_fixtures.rb +++ /dev/null @@ -1,14 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require "rexml/document" -include REXML - -@d = Document.new(File.new(File.dirname(__FILE__) + "/fixtures/benchmark.xml")) - -@path_by_name = "foo/bar/foo/bar" -@path_by_idx = "foo/bar[5]" # "bar6" -@path_by_idx_idx = "foo/bar[3]/bar[4]" # "bar4-6" -@path_by_attr_idx = "foo/bar[@barkey='subtree']/bar[4]" # "bar4-6" -@path_by_attr = "@key" # "xy" - -@count=500 diff --git a/test/bookmarks.rb b/test/bookmarks.rb deleted file mode 100644 index 2cb1488..0000000 --- a/test/bookmarks.rb +++ /dev/null @@ -1,80 +0,0 @@ -class BMNode - attr_accessor :name - - def name=(x) - @name_set="#{x}_set" - @name=x - end - - def name - @name_get="#{@name}_get" - @name - end - - attr_reader :name_set, :name_get - - attr_accessor :last_changed - - def ==(other) - other.name==self.name and - other.last_changed==self.last_changed - end - - def initialize - yield(self) if block_given? - end -end - -class BMFolder < BMNode - attr_accessor :entries - - def ==(other) - super(other) and - self.entries == other.entries - end -end - -class BM < BMNode - attr_accessor :url - attr_accessor :refinement - - def ==(other) - super(other) and - self.url == other.url and - self.refinement == other.refinement - end -end - - - -require 'xml/mapping' - -module Mapping1 - class BMFolderMapping < BMFolder - include XML::Mapping - - root_element_name 'folder1' - - text_node :name, "@name" - numeric_node :last_changed, "@last-changed", :default_value=>nil - - array_node :entries, "entries1", "*" - end - - class BMMapping < BM - include XML::Mapping - - root_element_name 'bookmark1' - - text_node :name, "@bmname" - numeric_node :last_changed, "@bmlast-changed", :default_value=>nil - - text_node :url, "url" - object_node :refinement, "refinement", :default_value=>nil - end -end - - -module Mapping2 - # TODO -end diff --git a/test/company.rb b/test/company.rb deleted file mode 100644 index bb2a1e1..0000000 --- a/test/company.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'xml/mapping' - -# forward declarations -class Address; end -class Office; end -class Customer; end -class Thing; end - - -class Company - include XML::Mapping - - text_node :name, "@name" - - object_node :address, "address", :class=>Address - - array_node :offices, "offices", "office", :class=>Office - hash_node :customers, "customers", "customer", "@uid", :class=>Customer - - text_node :ent1, "arrtest/entry[1]" - text_node :ent2, "arrtest/entry[2]" - text_node :ent3, "arrtest/entry[3]" - - array_node :stuff, "stuff", "*" - array_node :things, "stuff2", "thing", :class=>Thing - - object_node :test_default_value_identity, "dummy", :default_value => ["default"] -end - - -class Address - include XML::Mapping - - text_node :city, "city" - numeric_node :zip, "zip", :default_value=>12576 - text_node :street, "street", :optional=>true - numeric_node :number, "number" -end - - -class Office - include XML::Mapping - - text_node :speciality, "@speciality" - boolean_node :classified, "classified", "yes", "no" - # object_node :address, "address", :class=>Address - object_node :address, "address", - :marshaller=>proc {|xml,value| value.fill_into_xml(xml)}, - :unmarshaller=>proc {|xml| Address.load_from_xml(xml)} -end - - -class Customer - include XML::Mapping - - text_node :uid, "@uid" - text_node :name, "name" -end - - -class Thing - include XML::Mapping - - choice_node 'name', (text_node :name, 'name'), - '@name', (text_node :name, '@name'), - :else, (text_node :name, '.') -end - - -class Names1 - include XML::Mapping - - choice_node :if, 'name', :then, (text_node :name, 'name'), - :elsif, 'names/name', :then, (array_node :names, 'names', 'name', :class=>String) -end - - -class ReaderTest - include XML::Mapping - - attr_accessor :read - - text_node :foo, "foo" - text_node :foo2, "foo2", :reader=>proc{|obj,xml| (obj.read||=[]) << :foo2 } - text_node :foo3, "foo3", :reader=>proc{|obj,xml,default| - (obj.read||=[]) << :foo3 - default.call(obj,xml) - } - text_node :bar, "bar" -end - - -class WriterTest - include XML::Mapping - - text_node :foo, "foo" - text_node :foo2, "foo2", :writer=>proc{|obj,xml| e = xml.elements.add; e.name='quux'; e.text='dingdong2' } - text_node :foo3, "foo3", :writer=>proc{|obj,xml,default| - default.call(obj,xml) - e = xml.elements.add; e.name='quux'; e.text='dingdong3' - } - text_node :bar, "bar" -end - - -class ReaderWriterProcVsLambdaTest - include XML::Mapping - - attr_accessor :read, :written - - text_node :proc_2args, "proc_2args", :reader=>Proc.new{|obj,xml| - (obj.read||=[]) << :proc_2args - }, - :writer=>Proc.new{|obj,xml| - (obj.written||=[]) << :proc_2args - } - - text_node :proc_3args, "proc_3args", :reader=>Proc.new{|obj,xml,default| - (obj.read||=[]) << :proc_3args - default.call(obj,xml) - }, - :writer=>Proc.new{|obj,xml,default| - (obj.written||=[]) << :proc_3args - default.call(obj,xml) - } - - - text_node :lambda_2args, "lambda_2args", :reader=>lambda{|obj,xml| - (obj.read||=[]) << :lambda_2args - }, - :writer=>lambda{|obj,xml| - (obj.written||=[]) << :lambda_2args - } - - - text_node :lambda_3args, "lambda_3args", :reader=>lambda{|obj,xml,default| - (obj.read||=[]) << :lambda_3args - default.call(obj,xml) - }, - :writer=>lambda{|obj,xml,default| - (obj.written||=[]) << :lambda_3args - default.call(obj,xml) - } - - -end diff --git a/test/documents_folders.rb b/test/documents_folders.rb deleted file mode 100644 index 3af4a1b..0000000 --- a/test/documents_folders.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'xml/mapping' - -class Entry - include XML::Mapping - - text_node :name, "name" - - def initialize(init_props={}) - super() #the super initialize (inherited from XML::Mapping) must be called - init_props.entries.each do |name,value| - self.send :"#{name}=", value - end - end -end - - -class Document [] - - def [](name) - entries.select{|e|e.name==name}[0] - end - - def append(name,entry) - entries << entry - entry.name = name - entry - end - - def ==(other) - Folder===other and - other.name==self.name and - other.entries==self.entries - end -end diff --git a/test/examples_test.rb b/test/examples_test.rb deleted file mode 100644 index ffb534d..0000000 --- a/test/examples_test.rb +++ /dev/null @@ -1,29 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" -$:.unshift File.dirname(__FILE__)+"/../examples" - -require 'test/unit' -require 'xml/xxpath_methods' - -require 'xml/mapping' - -# unit tests for some code in ../examples. Most of that code is -# included in ../README and tested when regenerating the README, but -# some things are better done outside. -class ExamplesTest < Test::Unit::TestCase - - def test_time_node - require 'time_node' - require 'order_signature_enhanced' - - s = Signature.load_from_file File.dirname(__FILE__)+"/../examples/order_signature_enhanced.xml" - assert_equal Time.local(2005,2,13), s.signed_on - - s.signed_on = Time.local(2006,6,15) - xml2 = s.save_to_xml - - assert_equal "15", xml2.first_xpath("signed-on/day").text - assert_equal "6", xml2.first_xpath("signed-on/month").text - assert_equal "2006", xml2.first_xpath("signed-on/year").text - end - -end diff --git a/test/fixtures/benchmark.xml b/test/fixtures/benchmark.xml deleted file mode 100644 index b29d799..0000000 --- a/test/fixtures/benchmark.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - x - bar1 - - y - - - bar2 - - - quux1 - - - bar3 - - - bar4 - - bar4-1 - - - z - - bar4-2 - - hello - - bar4-3 - - - - bar4-4 - - - bar4-5 - - - bar4-quux1 - - - bar4-6 - - - bar4-7 - - - - quux2 - - This buffer is for notes you don't want to save, and for Lisp - evaluation. If you want to create a file, first visit that file - with C-x C-f, then enter the text in that file's own buffer. - - bar5 - - - bar6 - - - quux3 - - - quux4 - - - bar7 - - - bar8 - - - bar9 - - - diff --git a/test/fixtures/bookmarks1.xml b/test/fixtures/bookmarks1.xml deleted file mode 100644 index 4e488f8..0000000 --- a/test/fixtures/bookmarks1.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - http://www.ruby-lang.org - - - - http://raa.ruby-lang.org/ - - - - - - http://www.slashdot.org/ - - - http://www.groklaw.net/ - - - - - diff --git a/test/fixtures/company1.xml b/test/fixtures/company1.xml deleted file mode 100644 index 9a596fd..0000000 --- a/test/fixtures/company1.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - -
- Berlin - 10176 - Unter den Linden - 12 -
- - - - no -
- Hamburg - 18282 - Feldweg - 28 -
-
- - yes -
- Baghdad - Airport - 18 -
-
-
- - - - - - James Kirk - cash - - - - Ernie - sweeties - - - - Cookie Monster - cookies - - - - Saddam Hussein - independent arms broker - - - - - - - foo - bar - baz - - - - - - Saddam Hussein - independent arms broker - -
- Berlin - 10176 - Unter den Linden - 12 -
- - yes -
- Baghdad - Airport - 18 -
-
-
- - - - - name2 - - name3 - name4-inlinename4-elt - -
diff --git a/test/fixtures/documents_folders.xml b/test/fixtures/documents_folders.xml deleted file mode 100644 index ccd524e..0000000 --- a/test/fixtures/documents_folders.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - home - - - plan - - inhale, exhale - eat, drink, sleep - - - - - work - - - xml-mapping - - - README - - XML-MAPPING: XML-to-object (and back) mapper - for Ruby, including XPath interpreter - - - - Rakefile - - rake away - - - - - - schedule - - 12:00 AM lunch - 4:00 PM appointment - - - - - - - books - - - The Hitchhiker's Guide to the Galaxy - - The Answer to Life, the Universe, and Everything - - - - Programming Ruby - - The Pragmatic Programmer's Guide - - - - - diff --git a/test/fixtures/documents_folders2.xml b/test/fixtures/documents_folders2.xml deleted file mode 100644 index e7fcf5b..0000000 --- a/test/fixtures/documents_folders2.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - home - - - plan - inhale, exhale - - - - work - - - xml-mapping - - - README - foo bar baz - - - - - diff --git a/test/fixtures/number.xml b/test/fixtures/number.xml deleted file mode 100644 index 89e49cd..0000000 --- a/test/fixtures/number.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/fixtures/triangle_m1.xml b/test/fixtures/triangle_m1.xml deleted file mode 100644 index 4bbaa0d..0000000 --- a/test/fixtures/triangle_m1.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - 3 - 0 - - - 2 - 4 - - - 0 - 1 - - green - diff --git a/test/fixtures/triangle_m2.xml b/test/fixtures/triangle_m2.xml deleted file mode 100644 index 8e1a389..0000000 --- a/test/fixtures/triangle_m2.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - tri1 - - - 3 - 0 - - - 2 - 4 - - - 0 - 1 - - - diff --git a/test/inheritance_test.rb b/test/inheritance_test.rb deleted file mode 100644 index 80d4463..0000000 --- a/test/inheritance_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' -require 'xml/mapping' -require 'xml/xxpath_methods' - -class Base - attr_accessor :baseinit - - def initialize(p) - self.baseinit = p - end -end - -class Derived < Base - include XML::Mapping - - text_node :mytext, "mytext" -end - -class Derived2 < Base - include XML::Mapping - - text_node :baseinit, "baseinit" -end - -# test that tries to reproduce ticket #4783 -class InheritanceTest < Test::Unit::TestCase - - def test_inheritance_simple - d = Derived.new "foo" - assert_equal "foo", d.baseinit - d.mytext = "hello" - dxml=d.save_to_xml - assert_equal "hello", dxml.first_xpath("mytext").text - d2 = Derived.load_from_xml(dxml) - assert_nil d2.baseinit - assert_equal "hello", d2.mytext - end - - def test_inheritance_superclass_initializing_mappedattr - d = Derived2.new "foo" - assert_equal "foo", d.baseinit - dxml=d.save_to_xml - assert_equal "foo", dxml.first_xpath("baseinit").text - d2 = Derived2.load_from_xml(dxml) - assert_equal "foo", d2.baseinit - end - -end diff --git a/test/multiple_mappings_test.rb b/test/multiple_mappings_test.rb deleted file mode 100644 index 1e5adf9..0000000 --- a/test/multiple_mappings_test.rb +++ /dev/null @@ -1,157 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' -require 'triangle_mm' - -require 'xml/xxpath_methods' - -class MultipleMappingsTest < Test::Unit::TestCase - def setup - # need to undo mapping class definitions that may have been - # established by other tests (and outlive those tests) - - # this requires some ugly hackery with internal variables - XML::Mapping.module_eval <<-EOS - Classes_by_rootelt_names.clear - EOS - Object.send(:remove_const, "Triangle") - unless ($".delete "triangle_mm.rb") - $".delete_if{|name| name =~ %r!test/triangle_mm.rb$!} - end - $:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb) - require 'triangle_mm' - end - - def test_read - t1=Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1 - assert_raises(XML::MappingError) do - Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m2 - end - t2=Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m2.xml", :mapping=>:m2 - - t=Triangle.new('tri1','green', - Point.new(3,0),Point.new(2,4),Point.new(0,1)) - - assert_equal t, t1 - assert_equal t, t2 - assert_equal t1, t2 - assert_equal "default description", t2.descr - assert_nil t1.descr - assert_not_equal Triangle.allocate, t - - # loading with default mapping should raise an exception because - # the default mapping was never used yet - assert_raises(XML::MappingError) do - Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m1.xml") - end - assert_raises(XML::MappingError) do - Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m2.xml") - end - - # after using it once, we get empty objects - Triangle.class_eval "use_mapping :_default" - assert_equal Triangle.allocate, - Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m1.xml") - assert_equal Triangle.allocate, - Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m2.xml") - end - - - def test_read_polymorphic - t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1 - t2=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m2.xml", :mapping=>:m2 - t=Triangle.new('tri1','green', - Point.new(3,0),Point.new(2,4),Point.new(0,1)) - - assert_equal t, t1 - assert_equal t, t2 - assert_equal t1, t2 - end - - - def test_write - t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1 - m1xml = t1.save_to_xml :mapping=>:m1 - m2xml = t1.save_to_xml :mapping=>:m2 - - assert_equal t1.name, m1xml.first_xpath('@name').text - assert_equal t1.name, m2xml.first_xpath('name').text - assert_equal t1.p2, Point.load_from_xml(m1xml.first_xpath('pt2'), :mapping=>:m1) - assert_equal t1.p2, Point.load_from_xml(m2xml.first_xpath('points/point[2]'), :mapping=>:m1) - end - - - def test_root_element - t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1 - m1xml = t1.save_to_xml :mapping=>:m1 - m2xml = t1.save_to_xml :mapping=>:m2 - - assert_equal "triangle", Triangle.root_element_name(:mapping=>:m1) - assert_equal "triangle", Triangle.root_element_name(:mapping=>:m2) - assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m1) - assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m2) - assert_equal "triangle", t1.save_to_xml(:mapping=>:m1).name - assert_equal "triangle", t1.save_to_xml(:mapping=>:m2).name - - Triangle.class_eval <<-EOS - use_mapping :m1 - root_element_name 'foobar' - EOS - - assert_equal "foobar", Triangle.root_element_name(:mapping=>:m1) - assert_equal "triangle", Triangle.root_element_name(:mapping=>:m2) - assert_nil XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m1) - assert_equal Triangle, XML::Mapping.class_for_root_elt_name("foobar",:mapping=>:m1) - assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m2) - assert_equal "foobar", t1.save_to_xml(:mapping=>:m1).name - assert_equal "triangle", t1.save_to_xml(:mapping=>:m2).name - assert_equal [Triangle,:m1], XML::Mapping.class_and_mapping_for_root_elt_name("foobar") - assert_raises(XML::MappingError) do - XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1) - end - assert_equal t1, XML::Mapping.load_object_from_xml(m2xml, :mapping=>:m2) - m1xml.name = "foobar" - assert_equal t1, XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1) - - Triangle.class_eval <<-EOS - use_mapping :m1 - root_element_name 'triangle' - EOS - - assert_raises(XML::MappingError) do - XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1) - end - m1xml.name = "triangle" - assert_equal t1, XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1) - end - - - def test_node_initialization - end - - - def test_misc - m1nodes = Triangle.xml_mapping_nodes(:mapping=>:m1).map{|n|n.class} - m2nodes = Triangle.xml_mapping_nodes(:mapping=>:m2).map{|n|n.class} - allnodes = Triangle.xml_mapping_nodes.map{|n|n.class} - - t=XML::Mapping::TextNode - o=XML::Mapping::ObjectNode - assert_equal [t,o,o,t,o], m1nodes - assert_equal [o,t,t,o,o,t], m2nodes - begin - assert_equal m1nodes+m2nodes, allnodes - rescue Test::Unit::AssertionFailedError - assert_equal m2nodes+m1nodes, allnodes - end - - allm1nodes = Triangle.all_xml_mapping_nodes(:mapping=>:m1).map{|n|n.class} - allm2nodes = Triangle.all_xml_mapping_nodes(:mapping=>:m2).map{|n|n.class} - allallnodes = Triangle.all_xml_mapping_nodes.map{|n|n.class} - - assert_equal allm1nodes, m1nodes - assert_equal allm2nodes, m2nodes - assert_equal allnodes, allallnodes - end - -end diff --git a/test/number.rb b/test/number.rb deleted file mode 100644 index 78f6c6e..0000000 --- a/test/number.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'xml/mapping' - -class Number - include XML::Mapping - - use_mapping :no_default - numeric_node :value, 'value' - - use_mapping :with_default - numeric_node :value, 'value', default_value: 0 -end diff --git a/test/rexml_xpath_benchmark.rb b/test/rexml_xpath_benchmark.rb deleted file mode 100644 index 8439f15..0000000 --- a/test/rexml_xpath_benchmark.rb +++ /dev/null @@ -1,29 +0,0 @@ -require File.dirname(__FILE__)+"/benchmark_fixtures" - -require "rexml/xpath" - -require 'benchmark' -include Benchmark - -rootelt = @d.root -foo2elt = rootelt.elements[3] -res1=res2=res3=res4=res5=nil -print "(#{@count} runs)\n" -bmbm(12) do |x| - x.report("by_name") { @count.times { res1 = XPath.first(rootelt, @path_by_name) } } - x.report("by_idx") { @count.times { res2 = XPath.first(rootelt, @path_by_idx) } } - x.report("by_idx_idx") { @count.times { res3 = XPath.first(rootelt, @path_by_idx_idx) } } - x.report("by_attr_idx") { @count.times { res4 = XPath.first(rootelt, @path_by_attr_idx) } } - x.report("xxpath_by_attr") { (@count*4).times { res5 = XPath.first(foo2elt, @path_by_attr) } } -end - - -def assert_equal(expected,actual) - expected==actual or raise "expected: #{expected.inspect}, actual: #{actual.inspect}" -end - -assert_equal "bar4-2", res1.text.strip -assert_equal "bar6", res2.text.strip -assert_equal "bar4-6", res3.text.strip -assert_equal "bar4-6", res4.text.strip -assert_equal "xy", res5.value.strip diff --git a/test/tests_init.rb b/test/tests_init.rb deleted file mode 100644 index cbf5192..0000000 --- a/test/tests_init.rb +++ /dev/null @@ -1,2 +0,0 @@ -$:.unshift File.dirname(__FILE__) -$:.unshift File.dirname(__FILE__)+"/../lib" diff --git a/test/triangle_mm.rb b/test/triangle_mm.rb deleted file mode 100644 index 204b26d..0000000 --- a/test/triangle_mm.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'xml/mapping' - -module XML::Mapping - def ==(other) - Marshal.dump(self) == Marshal.dump(other) - end -end - - -# forward declarations -class Point; end - -class Triangle - include XML::Mapping - - use_mapping :m1 - - text_node :name, "@name" - object_node :p1, "pt1", :class=>Point - object_node :p2, "points/point[2]", :class=>Point, :mapping=>:m2, :sub_mapping=>:m1 - object_node :p3, "pt3", :class=>Point - text_node :color, "color" - - - use_mapping :m2 - - text_node :color, "@color" - text_node :name, "name" - object_node :p1, "points/point[1]", :class=>Point, :sub_mapping=>:m1 - object_node :p2, "pt2", :class=>Point, :mapping=>:m1 - object_node :p3, "points/point[3]", :class=>Point, :sub_mapping=>:m1 - - text_node :descr, "description", :default_value=>"default description" - - def initialize(name,color,p1,p2,p3) - @name,@color,@p1,@p2,@p3 = name,color,p1,p2,p3 - end - - def ==(other) - name==other.name and color==other.color and - p1==other.p1 and p2==other.p2 and p3==other.p3 - end -end - - -class Point - include XML::Mapping - - use_mapping :m1 - - numeric_node :x, "x" - numeric_node :y, "y" - - def initialize(x,y) - @x,@y = x,y - end -end diff --git a/test/xml_mapping_adv_test.rb b/test/xml_mapping_adv_test.rb deleted file mode 100644 index f2371cf..0000000 --- a/test/xml_mapping_adv_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' -require 'documents_folders' -require 'bookmarks' - -class XmlMappingAdvancedTest < Test::Unit::TestCase - def setup - XML::Mapping.module_eval <<-EOS - Classes_by_rootelt_names.clear - EOS - Object.send(:remove_const, "Document") - Object.send(:remove_const, "Folder") - - unless ($".delete "documents_folders.rb") # works in 1.8 only. In 1.9, $" contains absolute paths. - $".delete_if{|name| name =~ %r!test/documents_folders.rb$!} - end - unless ($".delete "bookmarks.rb") - $".delete_if{|name| name =~ %r!test/bookmarks.rb$!} - end - require 'documents_folders' - require 'bookmarks' - - @f_xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/documents_folders2.xml")) - @f = XML::Mapping.load_object_from_xml(@f_xml.root) - - @bm1_xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/bookmarks1.xml")) - @bm1 = XML::Mapping.load_object_from_xml(@bm1_xml.root) - end - - def test_read_polymorphic_object - expected = Folder.new \ - :name => "home", - :entries => [ - Document.new(:name => "plan", :contents => " inhale, exhale"), - Folder.new(:name => "work", - :entries => [ - Folder.new(:name => "xml-mapping", - :entries => [Document.new(:name => "README", - :contents => "foo bar baz")] - ) - ]) - ] - - assert_equal expected, @f - end - - def test_write_polymorphic_object - xml = @f.save_to_xml - assert_equal "folder", xml.name - assert_equal "home", xml.elements[1].text - assert_equal "document", xml.elements[2].name - assert_equal "folder", xml.elements[3].name - assert_equal "name", xml.elements[3].elements[1].name - assert_equal "folder", xml.elements[3].elements[2].name - assert_equal "foo bar baz", xml.elements[3].elements[2].elements[2].elements[2].text - - @f.append "etc", Folder.new - @f["etc"].append "passwd", Document.new - @f["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh" - @f["etc"].append "hosts", Document.new - @f["etc"]["hosts"].contents = "127.0.0.1 localhost" - - xml = @f.save_to_xml - - xmletc = xml.elements[4] - assert_equal "etc", xmletc.elements[1].text - assert_equal "document", xmletc.elements[2].name - assert_equal "passwd", xmletc.elements[2].elements[1].text - assert_equal "foo:x:2:2:/bin/sh", xmletc.elements[2].elements[2].text - end - - def test_read_bookmars1_2 - expected = BMFolder.new{|x| - x.name = "root" - x.last_changed = 123 - x.entries = [ - BM.new{|x| - x.name="ruby" - x.last_changed=345 - x.url="http://www.ruby-lang.org" - x.refinement=nil - }, - BM.new{|x| - x.name="RAA" - x.last_changed=nil - x.url="http://raa.ruby-lang.org/" - x.refinement=nil - }, - BMFolder.new{|x| - x.name="news" - x.last_changed=nil - x.entries = [ - BM.new{|x| - x.name="/." - x.last_changed=233 - x.url="http://www.slashdot.org/" - x.refinement=nil - }, - BM.new{|x| - x.name="groklaw" - x.last_changed=238 - x.url="http://www.groklaw.net/" - x.refinement=nil - } - ] - } - ] - } - # need to compare expected==@bm1 because @bm1.== would be the - # XML::Mapping#== defined in xml_mapping_test.rb ... - assert_equal expected, @bm1 - assert_equal "root_set", @bm1.name_set - assert_equal "ruby_set", @bm1.entries[0].name_set - @bm1.entries[0].name = "foobar" - assert_equal "foobar_set", @bm1.entries[0].name_set - end -end - diff --git a/test/xml_mapping_test.rb b/test/xml_mapping_test.rb deleted file mode 100644 index 36074f9..0000000 --- a/test/xml_mapping_test.rb +++ /dev/null @@ -1,400 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' -require 'company' -require 'xml/xxpath_methods' - -module XML::Mapping - def ==(other) - Marshal.dump(self) == Marshal.dump(other) - end -end - -class XmlMappingTest < Test::Unit::TestCase - def setup - # need to undo mapping class definitions that may have been - # established by other tests (and outlive those tests) - - # this requires some ugly hackery with internal variables - XML::Mapping.module_eval <<-EOS - Classes_by_rootelt_names.clear - EOS - Object.send(:remove_const, "Company") - Object.send(:remove_const, "Address") - Object.send(:remove_const, "Office") - Object.send(:remove_const, "Customer") - Object.send(:remove_const, "Thing") - Object.send(:remove_const, "Names1") - Object.send(:remove_const, "ReaderTest") - Object.send(:remove_const, "WriterTest") - Object.send(:remove_const, "ReaderWriterProcVsLambdaTest") - unless ($".delete "company.rb") # works in 1.8 only. In 1.9, $" contains absolute paths. - $".delete_if{|name| name =~ %r!test/company.rb$!} - end - $:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb) - require 'company' - - @xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/company1.xml")) - @c = Company.load_from_xml(@xml.root) - end - - def test_getter_text_node - assert_equal "bar", @c.ent2 - end - - def test_getter_int_node - assert_equal 18, @c.offices[1].address.number - end - - def test_int_node_default_value - require 'number' - xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/number.xml")) - - assert_raise(XML::MappingError.new('no value, and no default value: Attribute value not set (text missing)')) do - Number.load_from_xml(xml.root, :mapping => :no_default) - end - - num = nil - assert_nothing_raised do - num = Number.load_from_xml(xml.root, :mapping => :with_default) - end - - assert_equal 0, num.value - end - - def test_getter_boolean_node - path=XML::XXPath.new("offices/office[2]/classified") - assert_equal(path.first(@xml.root).text == "yes", - @c.offices[1].classified) - end - - def test_getter_hash_node - assert_equal 4, @c.customers.keys.size - ["cm", "ernie", "jim", "sad"].zip(@c.customers.keys.sort).each do |exp,ckey| - assert_equal exp, ckey - assert_equal exp, @c.customers[ckey].uid - end - end - - - def test_getter_choice_node - assert_equal 4, @c.things.size - assert_equal "name1", @c.things[0].name - assert_equal "name2", @c.things[1].name - assert_equal "name3", @c.things[2].name - assert_equal "name4-elt", @c.things[3].name - end - - - def test_getter_choice_node_multiple_attrs - d = REXML::Document.new <<-EOS - - - multi1 - multi2 - - single - - EOS - n1 = Names1.load_from_xml d.root - assert_equal "single", n1.name - assert_nil n1.names - - d.root.delete_element "name" - n1 = Names1.load_from_xml d.root - assert_nil n1.name - assert_equal ["multi1","multi2"], n1.names - end - - - def test_choice_node_presence - node = Thing.xml_mapping_nodes[0] - t = Thing.new - assert !(node.is_present_in? t) - t.name = "Mary" - assert node.is_present_in? t - end - - - def test_getter_array_node - assert_equal ["pencils", "weapons of mass destruction"], - @c.offices.map{|o|o.speciality} - end - - - def test_reader - xml = REXML::Document.new(" - footext - foo2text - foo3text - bartext - ").root - r = ReaderTest.load_from_xml xml - assert_equal 'footext', r.foo - assert_nil r.foo2 - assert_equal 'foo3text', r.foo3 - assert_equal 'bartext', r.bar - assert_equal [:foo2,:foo3], r.read - - r.foo = 'foonew' - r.foo2 = 'foo2new' - r.foo3 = 'foo3new' - r.bar = 'barnew' - xml2 = r.save_to_xml - assert_equal 'foonew', xml2.first_xpath("foo").text - assert_equal 'foo2new', xml2.first_xpath("foo2").text - assert_equal 'foo3new', xml2.first_xpath("foo3").text - assert_equal 'barnew', xml2.first_xpath("bar").text - end - - - def test_writer - xml = REXML::Document.new(" - footext - foo2text - foo3text - bartext - ").root - w = WriterTest.load_from_xml xml - assert_equal 'footext', w.foo - assert_equal 'foo2text', w.foo2 - assert_equal 'foo3text', w.foo3 - assert_equal 'bartext', w.bar - - w.foo = 'foonew' - w.foo2 = 'foo2new' - w.foo3 = 'foo3new' - w.bar = 'barnew' - xml2 = w.save_to_xml - assert_equal 'foonew', xml2.first_xpath("foo").text - assert_nil xml2.first_xpath("foo2",:allow_nil=>true) - assert_equal 'foo3new', xml2.first_xpath("foo3").text - assert_equal 'barnew', xml2.first_xpath("bar").text - - assert_equal %w{dingdong2 dingdong3}, xml2.all_xpath("quux").map{|elt|elt.text} - end - - def test_reader_writer_proc_vs_lambda - xml = REXML::Document.new(" - proc_2args_text - lambda_2args_text - proc_3args_text - lambda_3args_text - ").root - r = ReaderWriterProcVsLambdaTest.load_from_xml xml - assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.read - assert_nil r.written - assert_nil r.proc_2args - assert_nil r.lambda_2args - assert_equal 'proc_3args_text', r.proc_3args - assert_equal 'lambda_3args_text', r.lambda_3args - - r.proc_2args = "proc_2args_text_new" - r.lambda_2args = "lambda_2args_text_new" - r.proc_3args = "proc_3args_text_new" - r.lambda_3args = "lambda_3args_text_new" - xml2 = r.save_to_xml - assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.written - assert_nil xml2.first_xpath("proc_2args", :allow_nil=>true) - assert_nil xml2.first_xpath("lambda_2args", :allow_nil=>true) - assert_equal 'proc_3args_text_new', xml2.first_xpath("proc_3args").text - assert_equal 'lambda_3args_text_new', xml2.first_xpath("lambda_3args").text - end - - def test_setter_text_node - @c.ent2 = "lalala" - assert_equal "lalala", REXML::XPath.first(@c.save_to_xml, "arrtest/entry[2]").text - end - - - def test_setter_array_node - xml=@c.save_to_xml - assert_equal ["pencils", "weapons of mass destruction"], - XML::XXPath.new("offices/office/@speciality").all(xml).map{|n|n.text} - end - - - def test_setter_hash_node - xml=@c.save_to_xml - assert_equal @c.customers.keys.sort, - XML::XXPath.new("customers/customer/@uid").all(@xml.root).map{|n|n.text}.sort - end - - - def test_setter_boolean_node - @c.offices[0].classified = !@c.offices[0].classified - xml=@c.save_to_xml - assert_equal @c.offices[0].classified, - XML::XXPath.new("offices/office[1]/classified").first(xml).text == "yes" - end - - - def test_setter_choice_node - xml=@c.save_to_xml - thingselts = xml.all_xpath("stuff2/thing") - assert_equal @c.things.size, thingselts.size - assert_equal @c.things[0].name, thingselts[0].first_xpath("name").text - assert_equal @c.things[1].name, thingselts[1].first_xpath("name").text - assert_equal @c.things[2].name, thingselts[2].first_xpath("name").text - assert_equal @c.things[3].name, thingselts[3].first_xpath("name").text - end - - - def test_setter_choice_node_multiple_attrs - n1 = Names1.new - assert_raises(XML::MappingError) { - n1.save_to_xml # no choice present in n1 - } - - n1.names = ["multi1","multi2"] - xml = n1.save_to_xml - assert_equal n1.names, xml.all_xpath("names/name").map{|elt|elt.text} - assert_nil xml.first_xpath("name", :allow_nil=>true) - - n1.name = "foo" - xml = n1.save_to_xml - assert_equal [], xml.all_xpath("names/name").map{|elt|elt.text} - assert_equal n1.name, xml.first_xpath("name", :allow_nil=>true).text - end - - - def test_root_element - assert_equal @c, XML::Mapping.load_object_from_file(File.dirname(__FILE__) + "/fixtures/company1.xml") - assert_equal @c, XML::Mapping.load_object_from_xml(@xml.root) - - assert_equal "company", Company.root_element_name - assert_equal Company, XML::Mapping.class_for_root_elt_name("company") - xml=@c.save_to_xml - assert_equal "company", xml.name - # Company.root_element_name 'my-test' - Company.class_eval <<-EOS - root_element_name 'my-test' - EOS - assert_equal "my-test", Company.root_element_name - assert_equal Company, XML::Mapping.class_for_root_elt_name("my-test") - assert_nil XML::Mapping.class_for_root_elt_name("company") - xml=@c.save_to_xml - assert_equal "my-test", xml.name - assert_equal "office", @c.offices[0].save_to_xml.name - - assert_raises(XML::MappingError) { - XML::Mapping.load_object_from_xml @xml.root - } - @xml.root.name = 'my-test' - assert_equal @c, XML::Mapping.load_object_from_xml(@xml.root) - - # white-box tests - #assert_equal [["my-test", {:_default=>Company}]], XML::Mapping::Classes_w_nondefault_rootelt_names.sort - #assert_equal [["address", {:_default=>Address}], - # ["company", {}], - # ["customer", {:_default=>Customer}], - # ["office", {:_default=>Office}]], - # XML::Mapping::Classes_w_default_rootelt_names.sort - end - - - def test_optional_flag - hamburg_address_path = XML::XXPath.new("offices/office[1]/address") - baghdad_address_path = XML::XXPath.new("offices/office[2]/address") - hamburg_zip_path = XML::XXPath.new("offices/office[1]/address/zip") - baghdad_zip_path = XML::XXPath.new("offices/office[2]/address/zip") - - assert_equal 18282, @c.offices[0].address.zip - assert_equal 12576, @c.offices[1].address.zip - xml=@c.save_to_xml - assert_equal "18282", hamburg_zip_path.first(xml).text - assert_nil baghdad_zip_path.first(xml,:allow_nil=>true) - @c.offices[1].address.zip = 12577 - xml=@c.save_to_xml - assert_equal "12577", baghdad_zip_path.first(xml).text - c2 = Company.load_from_xml(xml) - assert_equal 12577, c2.offices[1].address.zip - @c.offices[1].address.zip = 12576 - xml=@c.save_to_xml - assert_nil baghdad_zip_path.first(xml,:allow_nil=>true) - - hamburg_address_path.first(xml).delete_element("zip") - c3 = Company.load_from_xml(xml) - assert_equal 12576, c3.offices[0].address.zip - hamburg_address_path.first(xml).delete_element("city") - assert_raises(XML::MappingError) { - Company.load_from_xml(xml) - } - end - - - def test_optional_flag_nodefault - hamburg_address_path = XML::XXPath.new("offices/office[1]/address") - hamburg_street_path = XML::XXPath.new("offices/office[1]/address/street") - - assert_equal hamburg_street_path.first(@xml.root).text, - @c.offices[0].address.street - - hamburg_address_path.first(@xml.root).delete_element("street") - c2 = Company.load_from_xml(@xml.root) - assert_nil c2.offices[0].address.street - - xml2=c2.save_to_xml - assert_nil hamburg_street_path.first(xml2,:allow_nil=>true) - end - - - def test_default_value_identity_on_initialize - c = Company.new - assert_equal ["default"], c.test_default_value_identity - c.test_default_value_identity << "foo" - - c2 = Company.new - assert_equal ["default"], c2.test_default_value_identity - end - - - def test_default_value_identity_on_load - assert_equal ["default"], @c.test_default_value_identity - @c.test_default_value_identity << "bar" - - c2 = Company.load_from_file(File.dirname(__FILE__) + "/fixtures/company1.xml") - assert_equal ["default"], c2.test_default_value_identity - end - - - def test_polymorphic_node - assert_equal 3, @c.stuff.size - assert_equal 'Saddam Hussein', @c.stuff[0].name - assert_equal 'Berlin', @c.stuff[1].city - assert_equal 'weapons of mass destruction', @c.stuff[2].speciality - - @c.stuff[1].city = 'Munich' - @c.stuff[2].classified = false - - xml2=@c.save_to_xml - assert_equal 'Munich', xml2.root.elements[5].elements[2].elements[1].text - assert_equal 'no', xml2.root.elements[5].elements[3].elements[1].text - end - - - def test_file_io - require 'tmpdir' - Dir.mktmpdir do |dir| - @c.save_to_file "#{dir}/out.xml" - c2 = Company.load_from_file "#{dir}/out.xml" - assert_equal @c, c2, 'read back object equals original' - - @c.save_to_file "#{dir}/out_default.xml", :formatter=>REXML::Formatters::Default.new - - assert FileUtils.compare_file("#{dir}/out.xml", "#{dir}/out_default.xml"), 'default formatter is Formatters::Default' - assert File.open("#{dir}/out_default.xml").readlines.grep(/^\s/).empty?, 'default formatter produces no indentations' - - @c.save_to_file "#{dir}/out_pretty.xml", :formatter=>REXML::Formatters::Pretty.new - assert not(File.open("#{dir}/out_pretty.xml").readlines.grep(/^\s/).empty?), 'pretty formatter does produce indentations' - - Company.class_eval <<-EOS - mapping_output_formatter REXML::Formatters::Pretty.new - EOS - - @c.save_to_file "#{dir}/out2.xml" - assert FileUtils.compare_file("#{dir}/out2.xml", "#{dir}/out_pretty.xml"), 'default formatter can be changed on a per-class basis' - end - end - -end diff --git a/test/xpath_test.rb b/test/xpath_test.rb deleted file mode 100644 index 2c16317..0000000 --- a/test/xpath_test.rb +++ /dev/null @@ -1,427 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' - -require "rexml/document" -require "xml/xxpath" -require "xml/xxpath_methods" - - -class XPathTest < Test::Unit::TestCase - include REXML - - def setup - @d = Document.new <<-EOS - - x - bar1 - - y - - - bar2 - - bar3 - - - quux1 - - - bar4 - - - z - - bar5 - - - - - - EOS - end - - def test_read_byname - assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo").all(@d.root) - assert_equal @d.root.elements.to_a("foo")[1].elements.to_a("u"), XML::XXPath.new("foo/u").all(@d.root) - assert_equal [], XML::XXPath.new("foo/notthere").all(@d.root) - end - - - def test_read_byidx - assert_equal [@d.root.elements[1]], XML::XXPath.new("foo[1]").all(@d.root) - assert_equal [@d.root.elements[3]], XML::XXPath.new("foo[2]").all(@d.root) - assert_equal [], XML::XXPath.new("foo[10]").all(@d.root) - assert_equal [], XML::XXPath.new("foo[3]").all(@d.root) - end - - - def test_read_byall - assert_equal @d.root.elements.to_a, XML::XXPath.new("*").all(@d.root) - assert_equal [], XML::XXPath.new("notthere/*").all(@d.root) - end - - - def test_read_bythisnode - assert_equal [@d.root], XML::XXPath.new(".").all(@d.root) - assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo/.").all(@d.root) - assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo/./././.").all(@d.root) - assert_equal @d.root.elements.to_a("foo")[0], XML::XXPath.new("foo/.").first(@d.root) - assert_equal @d.root.elements.to_a("foo")[0], XML::XXPath.new("foo/./././.").first(@d.root) - end - - - def test_read_byattr - assert_equal [@d.root.elements[3]], XML::XXPath.new("foo[@key='xy']").all(@d.root) - assert_equal [], XML::XXPath.new("foo[@key='notthere']").all(@d.root) - assert_equal [], XML::XXPath.new("notthere[@key='xy']").all(@d.root) - end - - - def test_attribute - elt = @d.root.elements[3] - attr1 = XML::XXPath::Accessors::Attribute.new(elt,"key",false) - attr2 = XML::XXPath::Accessors::Attribute.new(elt,"key",false) - assert_not_nil attr1 - assert_not_nil attr2 - assert_equal attr1,attr2 # tests Attribute.== - assert_nil XML::XXPath::Accessors::Attribute.new(elt,"notthere",false) - assert_nil XML::XXPath::Accessors::Attribute.new(elt,"notthere",false) - newattr = XML::XXPath::Accessors::Attribute.new(elt,"new",true) - assert_not_nil newattr - assert_equal newattr, XML::XXPath::Accessors::Attribute.new(elt,"new",false) - newattr.text = "lala" - assert_equal "lala", elt.attributes["new"] - end - - def test_read_byattrname - assert_equal [XML::XXPath::Accessors::Attribute.new(@d.root.elements[3],"key",false)], - XML::XXPath.new("foo/@key").all(@d.root) - assert_equal [], XML::XXPath.new("foo/@notthere").all(@d.root) - end - - - def test_read_byidx_then_name - assert_equal [@d.root.elements[3].elements[1]], XML::XXPath.new("foo[2]/u").all(@d.root) - assert_equal [], XML::XXPath.new("foo[2]/notthere").all(@d.root) - assert_equal [], XML::XXPath.new("notthere[2]/u").all(@d.root) - assert_equal [], XML::XXPath.new("foo[3]/u").all(@d.root) - end - - - def test_read_alternative_names - assert_equal ["bar3","quux1","bar4"], - XML::XXPath.new("foo/bar/bar|quux").all(@d.root).map{|node|node.text.strip} - end - - - def test_read_attr - assert_equal [@d.root.elements[3]], - XML::XXPath.new(".[@key='xy']").all(@d.root.elements[3]) - assert_equal [@d.root.elements[3]], - XML::XXPath.new("self::*[@key='xy']").all(@d.root.elements[3]) - assert_equal [], - XML::XXPath.new(".[@key='xz']").all(@d.root.elements[3]) - - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/.[@key='xy']") - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/self::*[@key='xy']") - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/*/.[@key='xy']") - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/*/self::*[@key='xy']") - assert_equal [], @d.all_xpath("bla/foo[2]/.[@key='xy2']") - assert_equal [], @d.all_xpath("bla/foo[2]/.[@key2='xy']") - assert_equal [], @d.all_xpath("bla/foo[2]/self::*[@key2='xy']") - end - - - def test_read_textnodes - assert_equal ["bar3"], @d.root.all_xpath("foo[2]/bar/bar[1]/text()").map{|x|x.text.strip} - end - - - def test_read_descendant - assert_equal ["bar1","bar2","bar3","bar4","bar5"], - XML::XXPath.new("//bar").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar2","bar3"], - XML::XXPath.new("//bar[@barkey='hello']").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar2","bar5"], - XML::XXPath.new("//foo/bar").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar3","bar4"], - XML::XXPath.new("//bar/bar").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar2","bar3","bar4","bar5"], - XML::XXPath.new("foo//bar").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar2","bar3","bar4","bar5","bar5"], - XML::XXPath.new("//foo//bar").all(@d.root).map{|node|node.text.strip} - assert_equal ["z"], - XML::XXPath.new("//bar//foo").all(@d.root).map{|node|node.text.strip} - assert_equal ["bar2","bar3","quux1"], - XML::XXPath.new("//@barkey").all(@d.root).map{|node|node.parent.text.strip} - end - - - def test_read_first - assert_equal @d.root.elements[3].elements[1], XML::XXPath.new("foo[2]/u").first(@d.root) - end - - def test_read_first_nil - assert_equal nil, XML::XXPath.new("foo[2]/notthere").first(@d.root, :allow_nil=>true) - end - - def test_read_first_exception - assert_raises(XML::XXPathError) { - XML::XXPath.new("foo[2]/notthere").first(@d.root) - } - end - - - def test_write_noop - assert_equal @d.root.elements[1], XML::XXPath.new("foo").first(@d.root, :ensure_created=>true) - assert_equal @d.root.elements[3].elements[1], XML::XXPath.new("foo[2]/u").first(@d.root, :ensure_created=>true) - # TODO: deep-compare of REXML documents? - end - - def test_write_byname_then_name - s1 = @d.elements[1].elements.size - s2 = @d.elements[1].elements[1].elements.size - node = XML::XXPath.new("foo/new1").first(@d.root, :ensure_created=>true) - assert_equal "new1", node.name - assert node.attributes.empty? - assert_equal @d.elements[1].elements[1].elements[1], node - assert_equal s1, @d.elements[1].elements.size - assert_equal s2+1, @d.elements[1].elements[1].elements.size - end - - - def test_write_byidx - XML::XXPath.new("foo[2]").first(@d.root, :ensure_created=>true) - # TODO: deep-compare of REXML documents? - assert_equal 2, @d.root.elements.select{|elt| elt.name=="foo"}.size - node = XML::XXPath.new("foo[10]").first(@d.root, :ensure_created=>true) - assert_equal 10, @d.root.elements.select{|elt| elt.name=="foo"}.size - assert_equal "foo", node.name - end - - - def test_write_byattrname - elt = @d.root.elements[3] - s1 = elt.attributes.size - attr_key = XML::XXPath.new("foo[2]/@key").first(@d.root, :ensure_created=>true) - assert_equal elt.attributes["key"], attr_key.text - - attr_new = XML::XXPath.new("foo[2]/@new").first(@d.root, :ensure_created=>true) - attr_new.text = "haha" - assert_equal "haha", attr_new.text - assert_equal "haha", elt.attributes["new"] - assert_equal s1+1, elt.attributes.size - end - - - def test_write_byname_and_attr - node1 = XML::XXPath.new("hiho[@blubb='bla']").first(@d.root,:ensure_created=>true) - node2 = XML::XXPath.new("hiho[@blubb='bla']").first(@d.root,:ensure_created=>true) - node3 = XML::XXPath.new("hiho[@blubb2='bla2']").first(@d.root,:ensure_created=>true) - assert_equal node1, node2 - assert_equal node2, node3 - assert_equal "hiho", node1.name - assert_equal 4, @d.root.elements.size - assert_equal @d.root.elements[4], node1 - assert_equal @d.root.elements[4], node3 - assert_equal 'bla', node3.attributes['blubb'] - assert_equal 'bla2', node3.attributes['blubb2'] - - node4 = XML::XXPath.new("hiho[@blubb='foo42']").first(@d.root,:ensure_created=>true) - assert_not_equal node3, node4 - assert_equal 5, @d.root.elements.size - assert_equal @d.root.elements[5], node4 - assert_equal 'foo42', node4.attributes['blubb'] - end - - - def test_write_alternative_names - node = XML::XXPath.new("foo/bar/bar|quux").first(@d.root,:ensure_created=>true) - assert_equal XML::XXPath.new("foo/bar/bar").first(@d.root), node - - node = XML::XXPath.new("foo/bar/bar|quux").create_new(@d.root) - assert node.unspecified? - end - - - def test_write_attr - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/.[@key='xy']", :ensure_created=>true) - assert_equal "xy", @d.root.elements[3].attributes['key'] - assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/self::*[@key2='ab']", :ensure_created=>true) - assert_equal "ab", @d.root.elements[3].attributes['key2'] - assert_equal "xy", @d.root.elements[3].attributes['key'] - - assert_raises(XML::XXPathError) { - @d.root.elements[3].create_new_xpath ".[@key='xy']" - } - assert_raises(XML::XXPathError) { - @d.root.elements[3].create_new_xpath "self::*[@notthere='foobar']" - } - end - - - def test_write_textnodes - @d.root.create_new_xpath("morestuff/text()").text = "hello world" - assert_equal "hello world", @d.root.first_xpath("morestuff").text - end - - - def test_write_descendant - assert_equal @d.root.elements[3].elements[2].elements[2], - node1 = XML::XXPath.new("//bar[@barkey='hello']//quux").first(@d.root,:ensure_created=>true) - node1 = XML::XXPath.new("//bar[@barkey='hello']/hiho").first(@d.root,:ensure_created=>true) - assert_equal "hiho", node1.name - assert_equal @d.root.elements[3].elements[2], node1.parent - - node1 = XML::XXPath.new("/foo//quux/new").first(@d.root,:ensure_created=>true) - assert_equal "new", node1.name - assert_equal @d.root.elements[3].elements[2].elements[2], node1.parent - - assert_raises(XML::XXPathError) { - XML::XXPath.new("//bar[@barkey='hello']//new2").first(@d.root,:ensure_created=>true) - } - end - - - def test_write_bythisnode - s1 = @d.elements[1].elements.size - s2 = @d.elements[1].elements[1].elements.size - node = XML::XXPath.new("foo/././.").first(@d.root, :ensure_created=>true) - assert_equal @d.elements[1].elements[1], node - - node = XML::XXPath.new("foo/new1/././.").first(@d.root, :ensure_created=>true) - assert_equal "new1", node.name - assert node.attributes.empty? - assert_equal @d.elements[1].elements[1].elements[1], node - assert_equal s1, @d.elements[1].elements.size - assert_equal s2+1, @d.elements[1].elements[1].elements.size - end - - - def test_create_new_byname - s1 = @d.elements[1].elements.size - s2 = @d.elements[1].elements[1].elements.size - startnode = @d.elements[1].elements[1] - node1 = XML::XXPath.new("new1").create_new(startnode) - node2 = XML::XXPath.new("new1").first(startnode, :create_new=>true) #same as .create_new(...) - assert_equal "new1", node1.name - assert_equal "new1", node2.name - assert node1.attributes.empty? - assert node2.attributes.empty? - assert_equal @d.elements[1].elements[1].elements[1], node1 - assert_equal @d.elements[1].elements[1].elements[2], node2 - assert_equal s1, @d.elements[1].elements.size - assert_equal s2+2, @d.elements[1].elements[1].elements.size - end - - - def test_create_new_byname_then_name - s1 = @d.elements[1].elements.size - node1 = XML::XXPath.new("foo/new1").create_new(@d.root) - node2 = XML::XXPath.new("foo/new1").create_new(@d.root) - assert_equal "new1", node1.name - assert_equal "new1", node2.name - assert node1.attributes.empty? - assert node2.attributes.empty? - assert_equal @d.elements[1].elements[s1+1].elements[1], node1 - assert_equal @d.elements[1].elements[s1+2].elements[1], node2 - assert_equal s1+2, @d.elements[1].elements.size - end - - - def test_create_new_byidx - assert_raises(XML::XXPathError) { - XML::XXPath.new("foo[2]").create_new(@d.root) - } - node1 = XML::XXPath.new("foo[3]").create_new(@d.root) - assert_raises(XML::XXPathError) { - XML::XXPath.new("foo[3]").create_new(@d.root) - } - assert_equal @d.elements[1].elements[4], node1 - assert_equal "foo", node1.name - node2 = XML::XXPath.new("foo[4]").create_new(@d.root) - assert_equal @d.elements[1].elements[5], node2 - assert_equal "foo", node2.name - node3 = XML::XXPath.new("foo[10]").create_new(@d.root) - assert_raises(XML::XXPathError) { - XML::XXPath.new("foo[10]").create_new(@d.root) - } - XML::XXPath.new("foo[11]").create_new(@d.root) - assert_equal @d.elements[1].elements[11], node3 - assert_equal "foo", node3.name - # @d.write - end - - def test_create_new_byname_then_idx - node1 = XML::XXPath.new("hello/bar[3]").create_new(@d.root) - node2 = XML::XXPath.new("hello/bar[3]").create_new(@d.root) - # same as create_new - node3 = XML::XXPath.new("hello/bar[3]").create_new(@d.root) - assert_equal @d.elements[1].elements[4].elements[3], node1 - assert_equal @d.elements[1].elements[5].elements[3], node2 - assert_equal @d.elements[1].elements[6].elements[3], node3 - assert_not_equal node1, node2 - assert_not_equal node1, node3 - assert_not_equal node2, node3 - end - - - def test_create_new_byattrname - node1 = XML::XXPath.new("@lala").create_new(@d.root) - assert_raises(XML::XXPathError) { - XML::XXPath.new("@lala").create_new(@d.root) - } - assert node1.kind_of?(XML::XXPath::Accessors::Attribute) - node1.text = "val1" - assert_equal "val1", @d.elements[1].attributes["lala"] - foo2 = XML::XXPath.new("foo[2]").first(@d.root) - assert_raises(XML::XXPathError) { - XML::XXPath.new("@key").create_new(foo2) - } - node2 = XML::XXPath.new("@bar").create_new(foo2) - assert node2.kind_of?(XML::XXPath::Accessors::Attribute) - node2.text = "val2" - assert_equal "val2", @d.elements[1].elements[3].attributes["bar"] - end - - - def test_create_new_byname_and_attr - node1 = XML::XXPath.new("hiho[@blubb='bla']").create_new(@d.root) - node2 = XML::XXPath.new("hiho[@blubb='bla']").create_new(@d.root) - node3 = XML::XXPath.new("hiho[@blubb2='bla']").create_new(@d.root) - assert_equal "hiho", node1.name - assert_equal "hiho", node2.name - assert_equal @d.root.elements[4], node1 - assert_equal @d.root.elements[5], node2 - assert_equal @d.root.elements[6], node3 - assert_not_equal @d.root.elements[5], node1 - end - - - def test_create_new_bythisnode - s1 = @d.elements[1].elements.size - s2 = @d.elements[1].elements[1].elements.size - startnode = @d.elements[1].elements[1] - assert_raises(XML::XXPathError) { - node1 = XML::XXPath.new("new1/.").create_new(startnode) - } - assert_raises(XML::XXPathError) { - node2 = XML::XXPath.new("new1/././.").first(startnode, :create_new=>true) - } - end - - - def test_unspecifiedness - node1 = XML::XXPath.new("foo/hello").create_new(@d.root) - assert(!(node1.unspecified?)) - assert_equal @d.root, node1.parent.parent - node2 = XML::XXPath.new("foo/*").create_new(@d.root) - assert_equal @d.root, node2.parent.parent - assert node2.unspecified? - node2.name = "newone" - assert_equal "newone", node2.name - assert(!(node2.unspecified?)) - end - -end diff --git a/test/xxpath_benchmark.rb b/test/xxpath_benchmark.rb deleted file mode 100644 index 32d17df..0000000 --- a/test/xxpath_benchmark.rb +++ /dev/null @@ -1,36 +0,0 @@ -require File.dirname(__FILE__)+"/benchmark_fixtures" - -require "xml/xxpath" - -require 'benchmark' -include Benchmark - - -xxpath_by_name = XML::XXPath.new(@path_by_name) -xxpath_by_idx = XML::XXPath.new(@path_by_idx) # "bar6" -xxpath_by_idx_idx = XML::XXPath.new(@path_by_idx_idx) # "bar4-6" -xxpath_by_attr_idx = XML::XXPath.new(@path_by_attr_idx) # "bar4-6" -xxpath_by_attr = XML::XXPath.new(@path_by_attr) # "xy" - -rootelt = @d.root -foo2elt = rootelt.elements[3] -res1=res2=res3=res4=res5=nil -print "(#{@count} runs)\n" -bmbm(12) do |x| - x.report("by_name") { @count.times { res1 = xxpath_by_name.first(rootelt) } } - x.report("by_idx") { @count.times { res2 = xxpath_by_idx.first(rootelt) } } - x.report("by_idx_idx") { @count.times { res3 = xxpath_by_idx_idx.first(rootelt) } } - x.report("by_attr_idx") { @count.times { res4 = xxpath_by_attr_idx.first(rootelt) } } - x.report("xxpath_by_attr") { (@count*4).times { res5 = xxpath_by_attr.first(foo2elt) } } -end - - -def assert_equal(expected,actual) - expected==actual or raise "expected: #{expected.inspect}, actual: #{actual.inspect}" -end - -assert_equal "bar4-2", res1.text.strip -assert_equal "bar6", res2.text.strip -assert_equal "bar4-6", res3.text.strip -assert_equal "bar4-6", res4.text.strip -assert_equal "xy", res5.text.strip diff --git a/test/xxpath_methods_test.rb b/test/xxpath_methods_test.rb deleted file mode 100644 index e774cb8..0000000 --- a/test/xxpath_methods_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require File.dirname(__FILE__)+"/tests_init" - -require 'test/unit' - -require "rexml/document" -require "xml/xxpath_methods" - - -class XXPathMethodsTest < Test::Unit::TestCase - include REXML - - def setup - @d = Document.new <<-EOS - - x - bar1 - - y - - - - EOS - end - - def test_first_xpath - pathstr = "foo[2]/u" - path = XML::XXPath.new(pathstr) - elt = path.first(@d.root) - assert_equal elt, @d.root.first_xpath(pathstr) - assert_equal elt, @d.root.first_xpath(path) - end - - def test_all_xpath - pathstr = "foo" - path = XML::XXPath.new(pathstr) - elts = path.all(@d.root) - assert_equal elts, @d.root.all_xpath(pathstr) - assert_equal elts, @d.root.all_xpath(path) - end - - def test_each_xpath - pathstr = "foo" - path = XML::XXPath.new(pathstr) - elts = [] - path.each(@d.root) do |elt| - elts << elt - end - elts_actual = [] - @d.root.each_xpath(pathstr) do |elt| - elts_actual << elt - end - assert_equal elts, elts_actual - end - - def test_create_new - @d.root.create_new_xpath("foo") - @d.root.create_new_xpath(XML::XXPath.new("foo")) - assert_equal 4, @d.root.elements.to_a("foo").size - end - -end diff --git a/user_manual.in.md b/user_manual.in.md deleted file mode 100644 index b787a80..0000000 --- a/user_manual.in.md +++ /dev/null @@ -1,1086 +0,0 @@ -# XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter - -Xml-mapping is an easy to use, extensible library that allows you to -semi-automatically map Ruby objects to XML trees and vice versa. - -## Download - -For downloading the latest version, git repository access etc. go to: - -https://github.com/multi-io/xml-mapping - -## Contents of this Document - -- {Example}[rdoc-label:label-Example] -- {Single-attribute Nodes}[rdoc-label:label-Single-attribute+Nodes] - - {Default Values}[rdoc-label:label-Default+Values] - - {Single-attribute Nodes with Sub-objects}[rdoc-label:label-Single-attribute+Nodes+with+Sub-objects] - - {Attribute Handling Details, Augmenting Existing Classes}[rdoc-label:label-Attribute+Handling+Details-2C+Augmenting+Existing+Classes] -- {Other Nodes}[rdoc-label:label-Other+Nodes] - - {choice_node}[rdoc-label:label-choice_node] - - {Readers/Writers}[rdoc-label:label-Readers-2FWriters] -- {Multiple Mappings per Class}[rdoc-label:label-Multiple+Mappings+per+Class] -- {Defining your own Node Types}[rdoc-label:label-Defining+your+own+Node+Types] -- {XPath Interpreter}[rdoc-label:label-XPath+Interpreter] - -## Example - -(example document stolen + extended from -http://www.castor.org/xml-mapping.html) - -### Input Document: - - :include: order.xml - -### Mapping Class Declaration: - - :include: order.rb - -### Usage: - - :include: order_usage.intout - -As shown in the example, you have to include XML::Mapping into a class -to turn it into a "mapping class". There are no other restrictions -imposed on mapping classes; you can add attributes and methods to -them, include additional modules in them, derive them from other -classes, derive other classes from them etc.pp. - -An instance of a mapping class can be created from/converted into an -XML node with methods like XML::Mapping::ClassMethods.load_from_xml, -XML::Mapping#save_to_xml, XML::Mapping::ClassMethods.load_from_file, -XML::Mapping#save_to_file. Special class methods like "text_node", -"array_node" etc., called *node* *factory* *methods*, may be called -from the body of the class definition to define instance attributes -that are automatically and bidirectionally mapped to subtrees of the -XML element an instance of the class is mapped to. - -## Single-attribute Nodes - -For example, in the definition - - class Address - include XML::Mapping - - text_node :city, "City" - text_node :state, "State" - numeric_node :zip, "ZIP" - text_node :street, "Street" - end - -the first call to #text_node creates an attribute named "city" which -is mapped to the text of the XML child element defined by the XPath -expression "City" (xml-mapping includes an XPath interpreter that can -also be used seperately; see below[aref:xpath]). When you create an -instance of +Address+ from an XML element (using -Address.load_from_file(file_name) or -Address.load_from_xml(rexml_element)), that instance's "city" -attribute will be set to the text of the XML element's "City" child -element. When you convert an instance of +Address+ into an XML -element, a sub-element "City" is added and its text is set to the -current value of the +city+ attribute. The other node types -(numeric_node, array_node etc.) work analogously. Generally said, when -an instance of the above +Address+ class is created from or converted -to an XML tree, each of the four nodes in the class maps some parts of -that XML tree to a single, specific attribute of the +Adress+ -instance. The name of that attribute is given in the first argument to -the node factory method. Such a node is called a "single-attribute -node". All node types that come with xml-mapping except one -(+choice_node+, which I'll talk about below) are single-attribute -nodes. - - -### Default Values - -For each single-attribute node you may define a default value -which will be set if there was no value defined for the attribute in -the XML source. - -From the example: - - class Signature - include XML::Mapping - - text_node :position, "Position", :default_value=>"Some Employee" - end - -The semantics of default values are as follows: - -- when creating a new instance from scratch: - - - attributes with default values are set to their default values - - - attributes without default values are left unset - - (when defining your own initializer, you'll have to call the - inherited _initialize_ method in order to get this behaviour) - -- when loading an instance from an XML document: - - - attributes without default values that are not represented in - the XML raise an error - - - attributes with default values that are not represented in the - XML are set to their default values - - - all other attributes are set to their respective values as - present in the XML - - -- when saving an instance to an XML document: - - - unset attributes without default values raise an error - - - attributes with default values that are set to their default - values are not saved - - - all other attributes are saved - - -This implies that: - -- attributes that are set to their respective default values are not - represented in the XML - -- attributes without default values must be set explicitly before - saving - - - -### Single-attribute Nodes with Sub-objects - -Single-attribute nodes of type +array_node+, +hash_node+, and -+object_node+ recursively map one or more subtrees of their XML to -sub-objects (e.g. array elements or hash values) of their -attribute. For example, with the line - - array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[] - -, an attribute named "signatures" is added to the surrounding class -(here: +Order+); the attribute will be an array whose elements -correspond to the XML sub-trees yielded by the XPath expression -"Signed-By/Signature" (relative to the tree corresponding to the -+Order+ instance). Each element will be of class +Signature+ -(internally, each element is created from its corresponding XML -subtree by just calling -Signature.load_from_xml(the_subtree)). The reason why the -path "Signed-By/Signature" is provided in two arguments instead of -just one combined one becomes apparent when marshalling the array -(along with the surrounding +Order+ object) back into a sequence of -XML elements. When that happens, "Signed-By" names the common base -element for all those elements, and "Signature" is the path that will -be duplicated for each element. For example, when the +signatures+ -attribute contains an array with 3 +Signature+ instances (let's call -them sig1, sig2, and sig3) in it, it will -be marshalled to an XML tree that looks like this: - - - - [marshalled object sig1] - - - [marshalled object sig2] - - - [marshalled object sig3] - - - -Internally, each +Signature+ instance is stored into its - sub-element by calling -the_signature_instance.fill_into_xml(the_sub_element). The -input document in the example above shows how this ends up looking. - -hash_nodes work similarly, but they define hash-valued attributes -instead of array-valued ones. - -object_nodes are the simplest of the three types of -single-attribute nodes with sub-objects. They just map a single given -subtree directly to their attribute value. See the example for -examples :) - -The mentioned methods +load_from_xml+ and +fill_into_xml+ are the only -methods classes must implement in order to be usable in the -:class=> keyword arguments to node factory methods. Mapping -classes (i.e. classes that include XML::Mapping) -automatically inherit those functions and can thus be readily used in -:class=> arguments, as shown for the +Signature+ class in the -+array_node+ call above. In addition to that, xml-mapping adds those -methods to some of Ruby's core classes, namely +String+ and +Numeric+ -(and thus +Float+, +Integer+, and +BigInt+). So you can also use -strings or numbers as sub-objects of attributes of +array_node+, -+hash_node+, or +object_node+ nodes. For example, say you have an XML -document like this one: - - :include: stringarray.xml - -, and you want to map all the names to a string array attribute -+names+, you could do it like this: - - :include: stringarray.rb - -usage: - - :include: stringarray_usage.intout - -As a side node, this feature actually makes +text_node+ and -+numeric_node+ special cases of +object_node+. For example, -text_node :attr, "path" is the same as object_node :attr, -"path", :class=>String. - - -#### Polymorphic Sub-objects, Marshallers/Unmarshallers - -Besides the :class keyword argument, there are alternative -ways for a single-attribute node with sub-objects to specify the way -the sub-objects are created from/marshalled into their subtrees. - -First, it's possible not to specify anything at all -- in that case, -the class of a sub-object will be automatically deduced from the root -element name of its subtree. This allows you to achieve a kind of -"polymorphic", late-bound way to decide about the sub-object's -class. The following example document contains a hierarchical, -recursive set of named "documents" and "folders", where folders hold a -set of entries, each of which may again be either a document or a -folder: - - :include: documents_folders.xml - -This can be mapped to Ruby like this: - - :include: documents_folders.rb - -Usage: - - :include: documents_folders_usage.intout - -As you see, the Folder#entries attribute is mapped via an -array_node that does not specify a :class or anything else to -govern the instantiation of the array's elements. This causes -xml-mapping to deduce the class of each array element from the root -element name of the corresponding XML tree. In this example, the root -element name is either "document" or "folder". The mapping between -root element names and class names is the one briefly described in -example[aref:example] at the beginning of this document -- the -unqualified class name is just converted to lower case and "dashed", -e.g. Foo::Bar::MyClass becomes "my-class"; and you may overwrite this -on a per-class basis by calling root_element_name -"the-new-name" in the class body. In our example, the root -element name "document" leads to an instantiation of class +Document+, -and the root element name "folder" leads to an instantiation of class -+Folder+. - -Incidentally, the last example shows that you can readily derive -mapping classes from one another (as said before, you can also derive -mapping classes from other classes, include other modules into them -etc. at will). This works just like intuition thinks it should -- when -deriving one mapping class from another one, the list of nodes in -effect when loading/saving instances of the derived class will consist -of all nodes of that class and all superclasses, starting with the -topmost superclass that has nodes defined. There is one thing to take -care of though: When deriving mapping classes from one another, you -have to make sure to include XML::Mapping in each class. This -requirement exists purely due to ease-of-implementation -considerations; there are probably ways to do away with it, but the -inconvenience seemed not severe enough for me to bother (as -yet). Still, you might get "strange" errors if you forget to do it for -a class. - -Besides the :class keyword argument and no argument, there is -a third way to specify the way the sub-objects are created -from/marshalled into their subtrees: :marshaller and/or -:unmarshaller keyword arguments. Here you pass procs in which -you just do all the work manually. So this is basically a "catch-all" -for cases where the other two alternatives are not appropriate for the -problem at hand. (*TODO*: Use other example?) Let's say we want to -extend the +Signature+ class from the initial example to include the -date on which the signature was created. We want the new XML -representation of such a signature to look like this: - - :include: time_node_w_marshallers.xml - -So, a new "signed-on" element was added that holds the day, month, and -year. In the +Signature+ instance in Ruby, we want the date to be -stored in an attribute named +signed_on+ of type +Time+ (that's Ruby's -built-in +Time+ class). - -One could think of using +object_node+, but something like -object_node :signed_on, "signed-on", :class=>Time won't work -because +Time+ isn't a mapping class and doesn't define methods -+load_from_xml+ and +fill_into_xml+ (we could easily define those -though; we'll talk about that possibility here[aref:attrdefns] and -here[aref:definingnodes]). The fastest, most ad-hoc way to achieve -what we want are :marshaller and :unmarshaller keyword arguments, like -this: - - :include: time_node_w_marshallers.intout - -The :unmarshaller proc will be called whenever a +Signature+ -instance is being read in from an XML source. The +xml+ argument -passed to the proc contains (as a REXML::Element instance) the XML -subtree corresponding to the node's attribute's sub-object currently -being read. In the case of our +object_node+, the sub-object is just -the node's attribute (+signed_on+) itself, and the subtree is the one -rooted at the element (if this were e.g. an +array_node+, -the :unmarshaller proc would be called once for each array -element, and +xml+ would hold the subtree corresponding to the -"current" array element). The proc is expected to extract the -sub-object's data from +xml+ and return the sub-object. So we have to -read the "year", "month", and "day" elements, construct a +Time+ -instance from them and return that. One could just use the REXML API -to do that, but I've decided here to use the XPath interpreter that -comes with xml-mapping (xml/xxpath), and specifically the -'xml/xxpath_methods' utility library that adds methods like +first+ to -REMXML::Element. We call +first+ on +xml+ three times, passing XPath -expressions to extract the "year"/"month"/"day" sub-elements, -construct the +Time+ instance from that and return it. The XPath -library is explained in more detail below[aref:xpath]. - -The :marshaller proc will be called whenever a +Signature+ -instance is being written into an XML tree. +xml+ is again the XML -subtree rooted at the element (it will still be empty when -this proc is called), and +value+ is the current value of the -sub-object (again, since this is an +object_node+, +value+ is the -node's attribute, i.e. the +Time+ instance). We have to fill +xml+ -with the data from +value+ here. So we add three elements "year", -"month" and "day" and set their texts to the corresponding values from -+value+. The commented-out code shows an alternative implementation of -the same thing using the XPath interpreter. - -It should be mentioned again that :marshaller/:unmarshaller procs are -possible with all single-attribute nodes with sub-objects, i.e. with -+object_node+, +array_node+, and +hash_node+. So, if you wanted to map -a whole array of date values, you could use +array_node+ with the same -:marshaller/:unmarshaller procs as above, for example: - - array_node :birthdays, "birthdays", "birthday", - :unmarshaller=> , - :marshaller=> - -You can see that :marshaller/:unmarshaller procs give you more -flexibility, but they also impose more work because you essentially -have to do all the work of marshalling/unmarshalling the sub-objects -yourself. If you find yourself copying and pasting -marshaller/unmarshaller procs all over the place, you should instead -define your own node type or mix the marshalling/unmarshalling -capabilities into the +Time+ class itself. This is explained -here[aref:attrdefns] and here[aref:definingnodes], and you'll see that -it's not really much more work than writing :marshaller and -:unmarshaller procs (you essentially just move the code from those -procs into your own node type resp. into the +Time+ class), so you -should not hesitate to do this. - -Another thing worth mentioning is that you don't have to specify -*both* a :marshaller and an :unmarshaller simultaneously. You can as -well give only one of them, and in addition to that pass a -:class argument or no argument. When you do that, the -specified marshaller (or unmarshaller) will be used when marshalling -(resp. unmarshalling) the sub-objects, and the other passed argument -(:class or none) will be employed when unmarshalling -(resp. marshalling) the sub-objects. So, in effect, you can deactivate -or "short-cut" some part of the marshalling/unmarshalling -functionality of a node type while retaining another part. - - - -### Attribute Handling Details, Augmenting Existing Classes - -I'll shed some more light on how single-attribute nodes add mapped -attributes to Ruby classes. An attribute declaration like - - text_node :city, "City" - -maps some portion of the XML tree (here: the "City" sub-element) to an -attribute (here: "city") of the class whose body the declaration -appears in. When writing (marshalling) instances of the surrounding -class into an XML document, xml-mapping will read the attribute value -from the instance using the function named +city+; when reading -(unmarshalling) an instance from an XML document, xml-mapping will use -the one-parameter function city= to set the attribute in the -instance to the value read from the XML document. - -If these functions don't exist at the time the node declaration is -executed, xml-mapping adds default implementations that simply -read/write the attribute value to instance variables that have the -same name as the attribute. For example, the +city+ attribute -declaration in the +Address+ class in the example added functions -+city+ and city= that read/write from/to the instance -variable @city. - -If, however, these functions already exist prior to defining the -attributes, xml-mapping will leave them untouched, so your precious -self-written accessor methods that do whatever complicated internal -processing of the data won't be overwritten. - -This means that you can not only create new mapping classes from -scratch, you can also take existing classes that contain some -"business logic" and "augment" them with xml-mapping capabilities. As -a simple example, let's augment Ruby's "Time" class with node -declarations that declare XML mappings for the day, month etc. fields: - - :include: time_augm.intout - -Here XML mappings are defined for the existing fields +year+, +month+ -etc. Xml-mapping noticed that the getter methods for those attributes -existed, so it didn't overwrite them. When calling +save_to_xml+ on a -+Time+ object, these methods are called and return the object's values -for those fields, which then get written to the output XML. - -So you can convert +Time+ objects into XML trees. What about reading -them back in from XML? All XML reading operations go through -.load_from_xml. The +load_from_xml+ class method -inherited from XML::Mapping (see -XML::Mapping::ClassMethods#load_from_xml) allocates a new instance of -the class (+Time+), then calls +fill_from_xml+ -(i.e. XML::Mapping#fill_from_xml) on it. +fill_from_xml+ iterates over -all our nodes in the order of their definition. For each node, its -data (the , or , or etc. element) is read from the -XML source and then written to the +Time+ instance via the respective -setter method (year=, month=, day= -etc.). These methods didn't exist in +Time+ before (+Time+ objects are -immutable), so xml-mapping defined its own, default setter methods -that just set @year, @month etc. This is of course -pretty useless because +Time+ objects don't hold their time in these -variables, so the setter methods don't really change the time of the -+Time+ object. So we have to redefine +load_from_xml+ for the +Time+ -class: - - :include: time_augm_loading.intout - - -## Other Nodes - -All nodes I've shown so far (node types text_node, numeric_node, -boolean_node, object_node, array_node, and hash_node) were -single-attribute nodes: The first parameter to the node factory method -of such a node is an attribute name, and the attribute of that name is -the only piece of the state of instances of the node's mapping class -that gets read/written by the node. - -### choice_node - -There is one node type distributed with xml-mapping that is not a -single-attribute node: +choice_node+. A +choice_node+ allows you to -specify a sequence of pairs, each consisting of an XPath expression -and another node (any node is supported here, including other -choice_nodes). When reading in an XML source, the choice_node will -delegate the work to the first node in the sequence whose -corresponding XPath expression was matched in the XML. When writing an -object back to XML, the choice_node will delegate the work to the -first node whose data was "present" in the object (for -single-attribute nodes, the data is considered "present" if the node's -attribute is non-nil; for choice_nodes, the data is considered -"present" if at least one of the node's sub-nodes is "present"). - -As a (somewhat contrived) example, here's a mapping for +Publication+ -objects that have either a single author (contained in an "author" XML -attribute) or several "contributors" (contained in a sequence of -"contr" XML elements): - - :include: publication.intout - -The symbols :if, :then, and :elsif (but not :else -- see below) in the -+choice_node+'s node factory method call are ignored; they may be -sprinkled across the argument list at will (preferably the way shown -above of course) to increase readability. - -The rest of the arguments specify the mentioned sequence of XPath -expressions and corresponding nodes. - -When reading a +Publication+ object from XML, the XPath expressions -from the +choice_node+ (@author and +contr+) will be matched -in sequence against the source XML tree until a match is found or the -end of the argument list is reached. If the end is reached, an -exception is raised. Otherwise, for the first XPath expression that -matched, the corresponding node will be invoked (i.e. used to read -actual data from the XML source into the +Person+ object). If you -specify :else, :default, or :otherwise in place of an XPath -expression, this is treated as an XPath expression that always -matches. So you can use :else (or :default or :otherwise) for a -"fallback" node that will be used if none of the other XPath -expressions matched (an example for this follows). - -When writing a +Publication+ object back to XML, the first node in the -sequence whose data is "present" in the source object will be invoked -to write data from the object into the target XML tree (and the -corresponding XPath expression will be created in the XML tree if it -doesn't exist already). If there is no such node in the sequence, an -exception is raised. As said above, for single-attribute nodes, the -node's data is considered "present" if the node's attribute is -non-nil. So, if you write a +Publication+ object to XML, and either -the +author+ or the +contributors+ attribute of the object is set, it -will be written; if both attributes are nil, an exception will be -raised. - -A frequent use case for choice_nodes will probably be object -attributes that may be represented in multiple alternative ways in -XML. As an example, consider "Person" objects where the name of the -person should be stored alternatively in a sub-element named +name+, -or an attribute named +name+, or in the text of the +person+ element -itself. You can achieve this with +choice_node+ like this: - - :include: person.intout - -Here all sub-nodes of the choice_nodes are single-attribute nodes -(text_nodes) with the same attribute (+name+). As you see, when -writing persons to XML, the name is always stored in a -sub-element. Of course, this is because that alternative appears first -in the choice_node. - - -### Readers/Writers - -Finally, _all_ nodes support keyword arguments :reader and :writer -which allow you to extend or completely override the reading and/or -writing functionality of the node with your own code. The :reader as -well as the :writer argument must be a proc that takes as its -arguments the Ruby object to be read/written (instance of the mapping -class the node belongs to) and the XML tree to be written to/read -from. An optional third argument may be specified -- it will receive a -proc that wraps the default reader/writer functionality of the -node. - -The :reader proc is for reading (from the XML into the object), the -:writer proc is for writing (from the object into the XML). - -Here's a (really contrived) example: - - :include: reader.intout - -So there's a "Foo" class with a text_node that would by default -(without the :reader and :writer proc) map the Ruby attribute "name" -to the XML attribute "name". The :reader proc is invoked when reading -from XML into a +Foo+ object. The +xml+ argument is the XML tree, -+obj+ is the object. +default_reader+ is the proc that wraps the -default reading functionality of the node. We invoke it at the -beginning. For this text_node, the default reading functionality is to -take the text of the "name" attribute of +xml+ and put it into the -+name+ attribute of +obj+. After that, we take the text of the "more" -attribute of +xml+ and append it to the +name+ attribute of +obj+. So -the XML tree is converted to a -+Foo+ object with +name+="JimXYZ". - -In our :writer proc, we only take +obj+ (the +Foo+ object to be -written to XML) and +xml+ (the XML tree the stuff is to be written -to). Analogously to the :reader, we could take a proc that wraps the -default writing functionality of the node, but we don't do that -here--we completely override the writing functionality with our own -code, which just takes the +name+ attribute of the object and writes -"hi ho" to a +bar+ XML attribute in the XML tree (stupid -example, I know). - -As a special convention, if you specify both a :reader and a :writer -for a node, and in both cases you do /not/ call the default behaviour, -then you should use the generic node type +node+, e.g.: - - class SomeClass - include XML::Mapping - - .... - - node :reader=>proc{|obj,xml| ...}, - :writer=>proc{|obj,xml| ...} - end - -(since you're completely replacing both the reading and the writing -functionality, you're effectively replacing all the functionality of -the node, so it would be pointless and confusing to use one of the -more "specific" node types) - -As you see, the purpose of readers and writers is to make it possible -to augment or override a node's functionality arbitrarily, so there -shouldn't be anything that's absolutely impossible to achieve with -xml-mapping. However, if you use readers and writers without invoking -the default behaviour, you really do everything manually, so you're -not doing any less work than you would do if you weren't using -xml-mapping at all. So you'll probably use readers and/or writers for -those bits of your mapping semantics that can't be achieved with -xml-mapping's predefined node types (an alternative approach might be -to override the +post_load+ and/or +post_save+ instance methods on the -mapping class -- see the reference documentation). - -An advice similar to the one given above for marshallers/unmarshallers -applies here as well: If you find yourself writing lots of readers and -writers that only differ in some easily parameterizable aspects, you -should think about defining your own node types. We talk about that -below[aref:definingnodes], and it generally just means that you move -the (sensibly parameterized) code from your readers/writers to your -node types. - - -## Multiple Mappings per Class - -Sometimes you might want to represent the same Ruby object in multiple -alternative ways in XML. For example, the name of a "Person" object -could be represented either in a "name" element or a "name" attribute. - -xml-mapping supports this by allowing you to define multiple disjoint -"mappings" for a mapping class. A mapping is by convention identified -with a symbol, e.g. :my_mapping, :other_mapping -etc., and each mapping comprises a root element name and a set of node -definitions. In the body of a mapping class definition, you switch to -another mapping with use_mapping :the_mapping. All following -node declarations will be added to that mapping *unless* you specify -the option :mapping=>:another_mapping for a node declaration (all node -types support that option). The default mapping (the mapping used if -there was no previous +use_mapping+ in the class body) is named -:_default. - -All the worker methods like load_from_xml/file, -save_to_xml/file, load_object_from_xml/file support -a :mapping keyword argument to specify the mapping, which -again defaults to :_default. - -In the following example, we define two mappings (the default one and -a mapping named :other) for +Person+ objects with a name, an -age and an address: - - :include: person_mm.intout - -In this example, each of the two mappings contains nodes that map the -same set of Ruby attributes (name, age and address). This is probably -what you want most of the time (since you're normally defining -multiple XML mappings for the same Ruby data), but it's not a -necessity at all. When a mapping class is defined, xml-mapping will -add all Ruby attributes from all mappings to it. - -You may have noticed that the object_nodes in the +Person+ -class apply the mapping they were themselves defined in to their -sub-ordinated class (+Address+). This is the case for all -{Single-attribute Nodes with Sub-objects}[aref:subobjnodes] -(+object_node+, +array_node+ and +hash_node+) unless you explicitly -specify a different mapping for the sub-object(s) using the option -:sub_mapping, e.g. - - object_node :address, "address", :class=>Address, :sub_mapping=>:other - - - -## Defining your own Node Types - -It's easy to write additional node types and register them with the -xml-mapping library (the following node types come with xml-mapping: -+node+, +text_node+, +numeric_node+, +boolean_node+, +object_node+, -+array_node+, +hash_node+, +choice_node+). - -I'll first show an example, then some more theoretical insight. - -### Example - -Let's say we want to extend the +Signature+ class from the example to -include the time at which the signature was created. We want the new -XML representation of such a signature to look like this: - - :include: order_signature_enhanced.xml - -(we only save year, month and day to make this example shorter), and -the mapping class declaration to look like this: - - :include: order_signature_enhanced.rb - -(i.e. a new "time_node" declaration was added). - -We want this +time_node+ call to define an attribute named +signed_on+ -which holds the date value from the XML document in an instance of -class +Time+. - -This node type can be defined with this piece of code: - - :include: time_node.rb - -The last line registers the new node type with the xml-mapping -library. The name of the node factory method ("time_node") is -automatically derived from the class name of the node type -("TimeNode"). - -There will be one instance of the node type +TimeNode+ per +time_node+ -declaration per mapping class (not per mapping class instance). That -instance (the "node" for short) will be created by the node factory -method (+time_node+); there's no need to instantiate the node type -directly. The +time_node+ method places the node into the mapping -class; the @owner attribute of the node is set to reference the -mapping class. The node factory method passes the mapping class the -node appears in (+Signature+), followed by its own arguments, to the -node's constructor. In the example, the +time_node+ method calls -TimeNode.new(Signature, :signed_on, "signed-on", -:default_value=>Time.now)). +new+ of course creates the node and -then delegates the arguments to our initializer +initialize+. We first -call the superclass's initializer, which strips off from the argument -list those arguments it handles itself, and returns the remaining -ones. In this case, the superclass XML::Mapping::SingleAttributeNode -handles the +Signature+, :signed_on and -:default_value=>Time.now arguments -- +Signature+ is stored -into @owner, :signed_on is stored into -@attrname, and {:default_value=>Time.now} is stored -into @options. The remaining argument list -["signed-on"] is returned; we capture the -"signed-on" string in _path_ (the rest of the argument list -(an empty array) we capture in _args_ for returning it at the end of -the initializer. This isn't strictly necessary, it's just a convention -that a node class initializer should always return those arguments it -didn't handle itself). We'll interpret _path_ as an XPath expression -that locates the time value relative to the parent mapping object's -XML tree (in this case, this would be the XML tree rooted at the - element, i.e. the tree the +Signature+ instance -was read from). We'll later have to read/store the year, month, and -day values from path+"/year", path+"/month", and -path+"/day", respectively, so we create (and precompile) -three corresponding XPath expressions using XML::XXPath.new and store -them into member variables of the node. XML::XXPath is an XPath -implementation that is bundled with xml-mapping. It is very -incomplete, but it supports writing (not just reading) of XML nodes, -which is needed to support writing data back to XML. The XML::XXPath -library is explained in more detail below[aref:xpath]. - -The +extract_attr_value+ method is called whenever an instance of the -mapping class the node belongs to (+Signature+ in the example) is -being created from an XML tree. The parameter _xml_ is that tree -(again, this is the tree rooted at the element in -this example). The method implementation is expected to extract the -single attribute's value from _xml_ and return it, or raise -XML::Mapping::SingleAttributeNode::NoAttrValueSet if the attribute was -"unset" in the XML (this exception tells the framework that the -default value should be put in place if it was defined), or raise any -other exception to signal an error and abort the whole process. Our -superclass XML::Mapping::SingleAttributeNode will store the returned -single attribute's value into the signed_on attribute of the -+Signature+ instance being read in. In our implementation, we apply -the xpath expressions created during initialization to _xml_ -(e.g. @y_path.first(xml)). An expression -_xpath_expr_.first(_xml_) returns (as a REXML element) the first -sub-element of _xml_ that matches _xpath_expr_, or raises -XML::XXPathError if there was no such element. We apply REXML's _text_ -method to the returned element to get out the element's text, convert -it to integer, and supply it to the constructor of the +Time+ object -to be returned. As a side note, if an XPath expression matches XML -attributes, XML::XXPath methods like _first_ will return -XML::XXPath::Accessors::Attribute nodes that behave similarly to -REXML::Element nodes, including support for messages like _name_ and -_text_, so this would've worked also if our XPath expressions had -referred to XML attributes, not elements. The +default_when_xpath_err+ -thing calls the supplied block and returns its value, but maps the -exception XML::XXPathError to the mentioned -XML::Mapping::SingleAttributeNode::NoAttrValueSet (any other -exceptions fall through unchanged). As said above, -XML::Mapping::SingleAttributeNode::NoAttrValueSet is caught by the -framework (more precisely, by our superclass -XML::Mapping::SingleAttributeNode), and the default value is set if it -was provided. So you should just wrap +default_when_xpath_err+ around -any applications of XPath expressions whose non-presence in the XML -you want to be considered a non-presence of the attribute you're -trying to extract. (XML::XXPath is designed to know knothing about -XML::Mapping, so it doesn't raise -XML::Mapping::SingleAttributeNode::NoAttrValueSet directly) - -The +set_attr_value+ method is called whenever an instance of the -mapping class the node belongs to (+Signature+ in the example) is -being stored into an XML tree. The _xml_ parameter is the XML tree (a -REXML element node; here this is again the tree rooted at the - element); _value_ is the current value of the -single attribute (in this example, the signed_on attribute of -the +Signature+ instance being stored). _xml_ will most probably be -"half-populated" by the time this method is called -- the framework -calls the +set_attr_value+ methods of all nodes of a mapping class in -the order of their definition, letting each node fill its "bit" into -_xml_. The method implementation is expected to write _value_ into -(the correct sub-elements of) _xml_, or raise an exception to signal -an error and abort the whole process. No default value handling is -done here; +set_attr_value+ won't be called at all if the attribute -had been set to its default value. In our implementation we grab the -year, month and day values from _value_ (which must be a +Time+), and -store it into the sub-elements of _xml_ identified by XPath -expressions @y_path, @m_path and @d_path, -respectively. We do this by calling XML::XXPath#first with an -additional parameter :ensure_created=>true. An expression -_xpath_expr_.first(_xml_,:ensure_created=>true) works just like -_xpath_expr_.first(_xml_) if _xpath_expr_ was already present in -_xml_. If it was not, it is created (preferably at the end of _xml_'s -list of sub-nodes), and returned. See below[aref:xpath] for a more -detailed documentation of the XPath interpreter. - -### Element order in created XML documents - -As just said, XML::XXPath, when used to create new XML nodes, -generally appends those nodes to the end of the list of subnodes of -the node the xpath expression was applied to. All xml-mapping nodes -that come with xml-mapping use XML::XXPath when writing data to XML, -and therefore also append their data to the XML data written by -preceding nodes (the nodes are invoked in the order of their -definition). This means that, generally, your output data will appear -in the XML document in the same order in which the corresponding -xml-mapping node definitions appeared in the mapping class (unless you -used XPath expressions like foo[number] which explicitly dictate a -fixed position in the sequence of XML nodes). For instance, in the -+Order+ class from the example at the beginning of this document, if -we put the :signatures node _before_ the :items -node, the element will appear _before_ the -sequence of elements in the output XML. - - -The following is a more systematic overview of the basic node -types. The description is self-contained, so some information from the -previous section will be repeated. - -### Node Types Are Ruby Classes - -A node type is implemented as a Ruby class derived from -XML::Mapping::Node or one of its subclasses. - -The following node types (node classes) come with xml-mapping (they -all live in the XML::Mapping namespace, which I've left out here for -brevity): - - Node - +-SingleAttributeNode - | +-SubObjectBaseNode - | | +-ObjectNode - | | +-ArrayNode - | | +-HashNode - | +-TextNode - | +-NumericNode - | +-BooleanNode - +-ChoiceNode - -XML::Mapping::Node is the base class for all nodes, -XML::Mapping::SingleAttributeNode is the base class for -{single-attribute nodes}[aref:sanodes], and -XML::Mapping::SubObjectBaseNode is the base class for -{single-attribute nodes with -sub-objects}[aref:subobjnodes]. XML::Mapping::TextNode, -XML::Mapping::ArrayNode etc. are of course the +text_node+, -+array_node+ etc. we've talked about in this document. When you've -written a new node class, you register it with xml-mapping by calling -XML::Mapping.add_node_class MyNode. When you do that, -xml-mapping automatically defines the node factory method for your -class -- the method's name (e.g. +my_node+) is derived from the node's -class name (e.g. Foo::Bar::MyNode) by stripping all parent module -names, and then converting capital letters to lowercase and preceding -them with an underscore. In fact, this is just how all the predefined -node types are defined -- those node types are not "special"; they're -defined in the source file +xml/mapping/standard_nodes.rb+ and then -registered normally in +xml/mapping.rb+. The source code of the -built-in nodes is not very long or complicated; you may consider -reading it in addition to this text to gain a better understanding. - - -### How Node Types Work - -The xml-mapping core "operates" node types as follows: - - -#### Node Initialization - -As said above, when a node class is registered with xml-mapping by -calling XML::Mapping.add_node_class TheNodeClass, xml-mapping -automatically generates the node factory method for that type. The -node factory method will effectively be defined as a class method of -the XML::Mapping module, which is why one can call it from the body of -a mapping class definition. The generated method will create a new -instance of the node class (a *node* for short) by calling _new_ on -the node class. The list of parameters to _new_ will consist of the -mapping class, followed by all arguments that were passed to the node -factory method. For example, when you have this node declaration: - - class MyMappingClass - include XML::Mapping - - my_node :foo, "bar", 42, :hi=>"ho" - end - -, then the node factory method (+my_node+) calls -MyNode.new(MyMappingClass, :foo, "bar", 42, :hi=>"ho"). - -_new_ of course creates the instance and calls _initialize_ on it. The -_initialize_ implementation will generally store the parameters into -some instance variables for later usage. As a convention, _initialize_ -should always extract from the parameter list those parameters it -processes itself, process them, and return an array containing the -remaining (still unprocessed) parameters. Thus, an implementation of -_initialize_ follows this pattern: - - def initialize(*args) - myparam1,myparam2,...,myparamx,*args = super(*args) - - .... process the myparam1,myparam2,...,myparamx .... - - # return still unprocessed args - args - end - -(since the called superclass initializer is written the same way, the -parameter array returned by it will already be stripped of all -parameters that the superclass initializer (or any of its -superclasses's initializers) processed) - -This technique is a simple way to "chain" the initializers of all -superclasses of a node class, starting with the topmost one (Node), so -that each initializer can easily find out and process the parameters -it is responsible for. - -The base node class XML::Mapping::Node provides an _initialize_ -implementation that, among other things (described below), adds _self_ -(i.e. the created node) to the internal list of nodes held by the -mapping class, and sets the @owner attribute of _self_ to reference -the mapping class. - -So, effectively there will be one instance of a node class (a node) -per node definition, and that instance lives in the mapping class the -node was defined in. - - -#### Node Operation during Marshalling and Unmarshalling - -When an instance of a mapping class is created or filled from an XML -tree, xml-mapping will call +xml_to_obj+ on all nodes defined in that -mapping class in the {mapping}[aref:mappings] the node is defined in, -in the order of their definition. Two parameters will be passed: the -mapping class instance being created/filled, and the XML tree the -instance is being created/filled from. The implementation of -+xml_to_obj+ is expected to read whatever pieces of data it is -responsible for from the XML tree and put it into the appropriate -variables/attributes etc. of the instance. - -When an instance of a mapping class is stored or filled into an XML -tree, xml-mapping will call +obj_to_xml+ on all nodes defined in that -mapping class in the {mapping}[aref:mappings] the node is defined in, -in the order of their definition, again passing as parameters the -mapping class instance being stored, and the XML tree the instance is -being stored/filled into. The implementation of +obj_to_xml+ is -expected to read whatever pieces of data it is responsible for from -the instance and put it into the appropriate XML elements/XML attr -etc. of the XML tree. - - -### Basic Node Types Overview - -The following is an overview of how initialization and -marshalling/unmarshalling is implemented in the node base classes -(Node, SingleAttributeNode, and SubObjectBaseNode). - -TODO: summary table: member var name; introduced in class; meaning - -#### Node - -In _initialize_, the mapping class and the option arguments are -stripped from the argument list. The mapping class is stored in -@owner, the option arguments are stored (as a hash) in @options (the -hash will be empty if no options were given). The -{mapping}[aref:mappings] the node is defined in is determined -(:mapping option, last use_mapping or :_default) and -stored in @mapping. The node then stores itself in the list of nodes -of the mapping class belonging to the mapping -(@owner.xml_mapping_nodes(:mapping=>@mapping); see -XML::Mapping::ClassMethods#xml_mapping_nodes). This list is the list -of nodes later used when marshalling/unmarshalling an instance of the -mapping class with respect to a given mapping. This means that node -implementors will not normally "see" anything of the mapping (they -don't need to access the @mapping variable) because the -marshalling/unmarshalling methods -(obj_to_xml/xml_to_obj) simply won't be called if -the node's mapping is not the same as the mapping the -marshalling/unmarshalling is happening with. - -Furthermore, if :reader and/or :writer options were given, -xml_to_obj resp. obj_to_xml are transparently -overwritten on the node to delegate to the supplied :reader/:writer -procs. - -The marshalling/unmarshalling methods -(obj_to_xml/xml_to_obj) are not implemented in -+Node+ (they just raise an exception). - - -#### SingleAttributeNode - -In _initialize_, the attribute name is stripped from the argument list -and stored in @attrname, and an attribute of that name is added to the -mapping class the node belongs to. - -During marshalling/unmarshalling of an object to/from XML, -single-attribute nodes only read/write a single piece of the object's -state: the single attribute (@attrname) the node handles. Because of -this, the obj_to_xml/xml_to_obj implementations in -SingleAttributeNode call two new methods introduced by -SingleAttributeNode, which must be overwritten by subclasses: - - extract_attr_value(xml) - - set_attr_value(xml, value) - -extract_attr_value(xml) is called by xml_to_obj -during unmarshalling. _xml_ is the XML tree being read. The method -must read the attribute's value from _xml_ and return -it. xml_to_obj will set the attribute to that value. - -set_attr_value(xml, value) is called by obj_to_xml -during marshalling. _xml_ is the XML tree being written, _value_ is -the current value of the attribute. The method must write _value_ into -(the correct sub-elements/attributes) of _xml_. - -SingleAttributeNode also handles the default value, if it was -specified (via the :default_value option): When writing data to XML, -set_attr_value(xml, value) won't be called if the attribute -was set to the default value. When reading data from XML, the -extract_attr_value(xml) implementation must raise a special -exception, XML::Mapping::SingleAttributeNode::NoAttrValueSet, if it -wants to indicate that the data was not present in the -XML. SingleAttributeNode will catch this exception and put the default -value, if it was defined, into the attribute. - - -#### SubObjectBaseNode - -The initializer will set up additional member variables @sub_mapping, -@marshaller, and @unmarshaller. - -@sub_mapping contains the mapping to be used when reading/writing the -sub-objects (either specified with :sub_mapping, or, by default, the -mapping the node itself was defined in). - -@marshaller and @unmarshaller contain procs that encapsulate -writing/reading of sub-objects to/from XML, as specified by the user -with :class/:marshaller/:unmarshaller etc. options (the meaning of -those different options was described {above}[aref:subobjnodes]). The -procs are there to be called from extract_attr_value or -set_attr_value whenever the need arises. - - -## XPath Interpreter - -XML::XXPath is an XPath parser. It is used in xml-mapping node type -definitions, but can just as well be utilized stand-alone (it does not -depend on xml-mapping). XML::XXPath is very incomplete and probably -will always be, but it should be reasonably efficient (XPath -expressions are precompiled), and, most importantly, it supports write -access, which is needed for writing objects to XML. For example, if -you create the path /foo/bar[3]/baz[@key='hiho'] in the XML -document - - - - hello - goodbye - - - -, you'll get: - - - - hello - goodbye - - - - - - - -XML::XXPath is explained in more detail in the reference documentation -and the user_manual_xxpath file. - - -## License - -xml-mapping is licensed under the Apache License, version 2.0. See the -LICENSE file for details. diff --git a/user_manual_md.html b/user_manual_md.html new file mode 100644 index 0000000..9506ce2 --- /dev/null +++ b/user_manual_md.html @@ -0,0 +1,2055 @@ + + + + + + +user_manual - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ +

XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter

+ +

Xml-mapping is an easy to use, extensible library that allows you to +semi-automatically map Ruby objects to XML trees and +vice versa.

+ +

Download

+ +

For downloading the latest version, git repository access etc. go to:

+ +

github.com/multi-io/xml-mapping

+ +

Contents of this Document

+ + +

Example

+ +

(example document stolen + extended from www.castor.org/xml-mapping.html)

+ +

Input Document:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<Order reference="12343-AHSHE-314159">
+  <Client>
+    <Name>Jean Smith</Name>
+    <Address where="home">
+      <City>San Mateo</City>
+      <State>CA</State>
+      <ZIP>94403</ZIP>
+      <Street>2000, Alameda de las Pulgas</Street>
+    </Address>
+    <Address where="work">
+      <City>San Francisco</City>
+      <State>CA</State>
+      <ZIP>94102</ZIP>
+      <Street>98765, Fulton Street</Street>
+    </Address>
+  </Client>
+
+  <Item reference="RF-0001">
+    <Description>Stuffed Penguin</Description>
+    <Quantity>10</Quantity>
+    <UnitPrice>8.95</UnitPrice>
+  </Item>
+
+  <Item reference="RF-0034">
+    <Description>Chocolate</Description>
+    <Quantity>5</Quantity>
+    <UnitPrice>28.50</UnitPrice>
+  </Item>
+
+  <Item reference="RF-3341">
+    <Description>Cookie</Description>
+    <Quantity>30</Quantity>
+    <UnitPrice>0.85</UnitPrice>
+  </Item>
+
+  <Signed-By>
+    <Signature>
+      <Name>John Doe</Name>
+      <Position>product manager</Position>
+    </Signature>
+
+    <Signature>
+      <Name>Jill Smith</Name>
+      <Position>clerk</Position>
+    </Signature>
+
+    <Signature>
+      <Name>Miles O'Brien</Name>
+    </Signature>
+  </Signed-By>
+
+</Order>
+ +

Mapping Class Declaration:

+ +
require 'xml/mapping'
+
+## forward declarations
+class Client; end
+class Address; end
+class Item; end
+class Signature; end
+
+
+class Order
+  include XML::Mapping
+
+  text_node :reference, "@reference"
+  object_node :client, "Client", :class=>Client
+  hash_node :items, "Item", "@reference", :class=>Item
+  array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
+
+  def total_price
+    items.values.map{|i| i.total_price}.inject(0){|x,y|x+y}
+  end
+end
+
+
+class Client
+  include XML::Mapping
+
+  text_node :name, "Name"
+  object_node :home_address, "Address[@where='home']", :class=>Address
+  object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil
+end
+
+
+class Address
+  include XML::Mapping
+
+  text_node :city, "City"
+  text_node :state, "State"
+  numeric_node :zip, "ZIP"
+  text_node :street, "Street"
+end
+
+
+class Item
+  include XML::Mapping
+
+  text_node :descr, "Description"
+  numeric_node :quantity, "Quantity"
+  numeric_node :unit_price, "UnitPrice"
+
+  def total_price
+    quantity*unit_price
+  end
+end
+
+
+class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+end
+
+ +

Usage:

+ +
####read access
+o=Order.load_from_file("order.xml") 
+=> #<Order:0x007ff64a0fe8b0 @signatures=[#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">], @reference="12343-AHSHE-314159", @client=#<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>, @items={"RF-0001"=>#<Item:0x007ff64a0df550 @descr="Stuffed Penguin", @quantity=10, @unit_price=8.95>, "RF-0034"=>#<Item:0x007ff64a0ddbd8 @descr="Chocolate", @quantity=5, @unit_price=28.5>, "RF-3341"=>#<Item:0x007ff64a0dc0d0 @descr="Cookie", @quantity=30, @unit_price=0.85>}>
+o.reference 
+=> "12343-AHSHE-314159"
+o.client 
+=> #<Client:0x007ff64a0fd6b8 @work_address=#<Address:0x007ff64a0ed678 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007ff64a0efef0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>
+o.items.keys 
+=> ["RF-0001", "RF-0034", "RF-3341"]
+o.items["RF-0034"].descr 
+=> "Chocolate"
+o.items["RF-0034"].total_price 
+=> 142.5
+o.signatures 
+=> [#<Signature:0x007ff64a0ce3e0 @position="product manager", @name="John Doe">, #<Signature:0x007ff64a0cd210 @position="clerk", @name="Jill Smith">, #<Signature:0x007ff649a322e8 @position="Some Employee", @name="Miles O'Brien">]
+o.signatures[2].name 
+=> "Miles O'Brien"
+o.signatures[2].position 
+=> "Some Employee"
+## default value was set
+
+o.total_price 
+=> 257.5
+
+####write access
+o.client.name="James T. Kirk"
+o.items['RF-4711'] = Item.new
+o.items['RF-4711'].descr = 'power transfer grid'
+o.items['RF-4711'].quantity = 2
+o.items['RF-4711'].unit_price = 29.95
+
+s=Signature.new
+s.name='Harry Smith'
+s.position='general manager'
+o.signatures << s
+xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) 
+=> <order reference='12343-AHSHE-314159'> ... </>
+xml.write($stdout,2) 
+<order reference='12343-AHSHE-314159'>
+  <Client>
+    <Name>
+      James T. Kirk
+    </Name>
+    <Address where='home'>
+      <City>
+        San Mateo
+      </City>
+      <State>
+        CA
+      </State>
+      <ZIP>
+        94403
+      </ZIP>
+      <Street>
+        2000, Alameda de las Pulgas
+      </Street>
+    </Address>
+    <Address where='work'>
+      <City>
+        San Francisco
+      </City>
+      <State>
+        CA
+      </State>
+      <ZIP>
+        94102
+      </ZIP>
+      <Street>
+        98765, Fulton Street
+      </Street>
+    </Address>
+  </Client>
+  <Item reference='RF-0001'>
+    <Description>
+      Stuffed Penguin
+    </Description>
+    <Quantity>
+      10
+    </Quantity>
+    <UnitPrice>
+      8.95
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-0034'>
+    <Description>
+      Chocolate
+    </Description>
+    <Quantity>
+      5
+    </Quantity>
+    <UnitPrice>
+      28.5
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-3341'>
+    <Description>
+      Cookie
+    </Description>
+    <Quantity>
+      30
+    </Quantity>
+    <UnitPrice>
+      0.85
+    </UnitPrice>
+  </Item>
+  <Item reference='RF-4711'>
+    <Description>
+      power transfer grid
+    </Description>
+    <Quantity>
+      2
+    </Quantity>
+    <UnitPrice>
+      29.95
+    </UnitPrice>
+  </Item>
+  <Signed-By>
+    <Signature>
+      <Name>
+        John Doe
+      </Name>
+      <Position>
+        product manager
+      </Position>
+    </Signature>
+    <Signature>
+      <Name>
+        Jill Smith
+      </Name>
+      <Position>
+        clerk
+      </Position>
+    </Signature>
+    <Signature>
+      <Name>
+        Miles O&apos;Brien
+      </Name>
+    </Signature>
+    <Signature>
+      <Name>
+        Harry Smith
+      </Name>
+      <Position>
+        general manager
+      </Position>
+    </Signature>
+  </Signed-By>
+</order>
+
+
+####Starting a new order from scratch
+o = Order.new 
+=> #<Order:0x007ff64a206050 @signatures=[]>
+## attributes with default values (here: signatures) are set
+## automatically
+
+xml=o.save_to_xml 
+XML::MappingError: no value, and no default value, for attribute: reference
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:724:in `obj_to_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:218:in `block in fill_into_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `each'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:217:in `fill_into_xml'
+    from /Users/oklischat/xml-mapping/lib/xml/mapping/base.rb:229:in `save_to_xml'
+## can't save as long as there are still unset attributes without
+## default values
+
+o.reference = "FOOBAR-1234"
+
+o.client = Client.new
+o.client.name = 'Ford Prefect'
+o.client.home_address = Address.new
+o.client.home_address.street = '42 Park Av.'
+o.client.home_address.city = 'small planet'
+o.client.home_address.zip = 17263
+o.client.home_address.state = 'Betelgeuse system'
+
+o.items={'XY-42' => Item.new}
+o.items['XY-42'].descr = 'improbability drive'
+o.items['XY-42'].quantity = 3
+o.items['XY-42'].unit_price = 299.95
+
+xml=o.save_to_xml
+xml.write($stdout,2)
+
+<order reference='FOOBAR-1234'>
+  <Client>
+    <Name>
+      Ford Prefect
+    </Name>
+    <Address where='home'>
+      <City>
+        small planet
+      </City>
+      <State>
+        Betelgeuse system
+      </State>
+      <ZIP>
+        17263
+      </ZIP>
+      <Street>
+        42 Park Av.
+      </Street>
+    </Address>
+  </Client>
+  <Item reference='XY-42'>
+    <Description>
+      improbability drive
+    </Description>
+    <Quantity>
+      3
+    </Quantity>
+    <UnitPrice>
+      299.95
+    </UnitPrice>
+  </Item>
+</order>
+## the root element name when saving an object to XML will by default
+## be derived from the class name (in this example, "Order" became
+## "order"). This can be overridden on a per-class basis; see
+## XML::Mapping::ClassMethods#root_element_name for details.
+ +

As shown in the example, you have to include XML::Mapping into a class to turn it into a +“mapping class”. There are no other restrictions imposed on mapping +classes; you can add attributes and methods to them, include additional +modules in them, derive them from other classes, derive other classes from +them etc.pp.

+ +

An instance of a mapping class can be created from/converted into an XML node with methods like XML::Mapping::ClassMethods#load_from_xml, +XML::Mapping#save_to_xml, +XML::Mapping::ClassMethods#load_from_file, +XML::Mapping#save_to_file. +Special class methods like “text_node”, “array_node” etc., called +node factory methods, may be called from the +body of the class definition to define instance attributes that are +automatically and bidirectionally mapped to subtrees of the XML element an instance of the class is mapped to.

+ +

Single-attribute Nodes

+ +

For example, in the definition

+ +
class Address
+  include XML::Mapping
+
+  text_node :city, "City"
+  text_node :state, "State"
+  numeric_node :zip, "ZIP"
+  text_node :street, "Street"
+end
+
+ +

the first call to text_node creates an attribute named “city” which is +mapped to the text of the XML child element defined +by the XPath expression “City” (xml-mapping includes an XPath interpreter +that can also be used seperately; see below). When +you create an instance of Address from an XML element (using Address.load_from_file(file_name) or +Address.load_from_xml(rexml_element)), that instance's “city” attribute +will be set to the text of the XML element's +“City” child element. When you convert an instance of Address +into an XML element, a sub-element “City” is added +and its text is set to the current value of the city +attribute. The other node types (numeric_node, array_node etc.) work +analogously. Generally said, when an instance of the above +Address class is created from or converted to an XML tree, each of the four nodes in the class maps some +parts of that XML tree to a single, specific +attribute of the Adress instance. The name of that attribute +is given in the first argument to the node factory method. Such a node is +called a “single-attribute node”. All node types that come with xml-mapping +except one (choice_node, which I'll talk about below) are +single-attribute nodes.

+ +

Default Values

+ +

For each single-attribute node you may define a default value +which will be set if there was no value defined for the attribute in the XML source.

+ +

From the example:

+ +
class Signature
+  include XML::Mapping
+
+  text_node :position, "Position", :default_value=>"Some Employee"
+end
+
+ +

The semantics of default values are as follows:

+
  • +

    when creating a new instance from scratch:

    +
    • +

      attributes with default values are set to their default values

      +
    • +

      attributes without default values are left unset

      +
    +
+ +

(when defining your own initializer, you'll have to call the inherited +initialize method in order to get this behaviour)

+
  • +

    when loading an instance from an XML document:

    +
    • +

      attributes without default values that are not represented in the XML raise an error

      +
    • +

      attributes with default values that are not represented in the XML are set to their default values

      +
    • +

      all other attributes are set to their respective values as present in the +XML

      +
    +
  • +

    when saving an instance to an XML document:

    +
    • +

      unset attributes without default values raise an error

      +
    • +

      attributes with default values that are set to their default values are +not saved

      +
    • +

      all other attributes are saved

      +
    +
+ +

This implies that:

+
  • +

    attributes that are set to their respective default values are not +represented in the XML

    +
  • +

    attributes without default values must be set explicitly before saving

    +
+ +

Single-attribute Nodes with Sub-objects

+ +

Single-attribute nodes of type array_node, +hash_node, and object_node recursively map one or +more subtrees of their XML to sub-objects (e.g. +array elements or hash values) of their attribute. For example, with the +line

+ +
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
+
+ +

, an attribute named “signatures” is added to the surrounding class (here: +Order); the attribute will be an array whose elements +correspond to the XML sub-trees yielded by the XPath +expression “Signed-By/Signature” (relative to the tree corresponding to the +Order instance). Each element will be of class +Signature (internally, each element is created from its +corresponding XML subtree by just calling +Signature.load_from_xml(the_subtree)). The reason why the path +“Signed-By/Signature” is provided in two arguments instead of just one +combined one becomes apparent when marshalling the array (along with the +surrounding Order object) back into a sequence of XML elements. When that happens, “Signed-By” names the +common base element for all those elements, and “Signature” is the path +that will be duplicated for each element. For example, when the +signatures attribute contains an array with 3 +Signature instances (let's call them sig1, +sig2, and sig3) in it, it will be marshalled to +an XML tree that looks like this:

+ +
<Signed-By>
+  <Signature>
+    [marshalled object sig1]
+  </Signature>
+  <Signature>
+    [marshalled object sig2]
+  </Signature>
+  <Signature>
+    [marshalled object sig3]
+  </Signature>
+</Signed-By>
+ +

Internally, each Signature instance is stored into its +<Signature> sub-element by calling +the_signature_instance.fill_into_xml(the_sub_element). The +input document in the example above shows how this ends up looking.

+ +

hash_nodes work similarly, but they define hash-valued +attributes instead of array-valued ones.

+ +

object_nodes are the simplest of the three types of +single-attribute nodes with sub-objects. They just map a single given +subtree directly to their attribute value. See the example for examples :)

+ +

The mentioned methods load_from_xml and +fill_into_xml are the only methods classes must implement in +order to be usable in the :class=> keyword arguments to +node factory methods. Mapping classes (i.e. classes that include +XML::Mapping) automatically inherit those functions and can thus be +readily used in :class=> arguments, as shown for the +Signature class in the array_node call above. In +addition to that, xml-mapping adds those methods to some of Ruby's core +classes, namely String and Numeric (and thus +Float, Integer, and BigInt). So you +can also use strings or numbers as sub-objects of attributes of +array_node, hash_node, or +object_node nodes. For example, say you have an XML document like this one:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<people>
+  <names>
+    <name>Jim</name>
+    <name>Susan</name>
+    <name>Herbie</name>
+    <name>Nancy</name>
+  </names>
+</people>
+ +

, and you want to map all the names to a string array attribute +names, you could do it like this:

+ +
require 'xml/mapping'
+class People
+  include XML::Mapping
+  array_node :names, "names", "name", :class=>String
+end
+
+ +

usage:

+ +
ppl=People.load_from_file("stringarray.xml") 
+=> #<People:0x007ff64a0cda08 @names=["Jim", "Susan", "Herbie", "Nancy"]>
+ppl.names 
+=> ["Jim", "Susan", "Herbie", "Nancy"]
+
+ppl.names.concat ["Mary","Arnold"] 
+=> ["Jim", "Susan", "Herbie", "Nancy", "Mary", "Arnold"]
+ppl.save_to_xml.write $stdout,2
+
+<people>
+  <names>
+    <name>
+      Jim
+    </name>
+    <name>
+      Susan
+    </name>
+    <name>
+      Herbie
+    </name>
+    <name>
+      Nancy
+    </name>
+    <name>
+      Mary
+    </name>
+    <name>
+      Arnold
+    </name>
+  </names>
+</people>
+ +

As a side node, this feature actually makes text_node and +numeric_node special cases of object_node. For +example, text_node :attr, "path" is the same as +object_node :attr, "path", :class=>String.

+ +

Polymorphic Sub-objects, Marshallers/Unmarshallers

+ +

Besides the :class keyword argument, there are alternative +ways for a single-attribute node with sub-objects to specify the way the +sub-objects are created from/marshalled into their subtrees.

+ +

First, it's possible not to specify anything at all – in that case, the +class of a sub-object will be automatically deduced from the root element +name of its subtree. This allows you to achieve a kind of “polymorphic”, +late-bound way to decide about the sub-object's class. The following +example document contains a hierarchical, recursive set of named +“documents” and “folders”, where folders hold a set of entries, each of +which may again be either a document or a folder:

+ +
<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<folder name="home">
+  <document name="plan">
+    <contents> inhale, exhale</contents>
+  </document>
+
+  <folder name="work">
+    <folder name="xml-mapping">
+      <document name="README">
+        <contents>foo bar baz</contents>
+      </document>
+    </folder>
+  </folder>
+
+</folder>
+ +

This can be mapped to Ruby like this:

+ +
require 'xml/mapping'
+
+class Entry
+  include XML::Mapping
+
+  text_node :name, "@name"
+end
+
+
+class Document <Entry
+  include XML::Mapping
+
+  text_node :contents, "contents"
+end
+
+
+class Folder <Entry
+  include XML::Mapping
+
+  array_node :entries, "document|folder", :default_value=>[]
+
+  def [](name)
+    entries.select{|e|e.name==name}[0]
+  end
+
+  def append(name,entry)
+    entries << entry
+    entry.name = name
+    entry
+  end
+end
+
+ +

Usage:

+ +
root = XML::Mapping.load_object_from_file "documents_folders.xml" 
+=> #<Folder:0x007ff6499c0f58 @entries=[#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">], @name="home">
+root.name 
+=> "home"
+root.entries 
+=> [#<Document:0x007ff6499bb8a0 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007ff6499ba298 @entries=[#<Folder:0x007ff6499b84e8 @entries=[#<Document:0x007ff6499b1fa8 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">]
+
+root.append "etc", Folder.new
+root["etc"].append "passwd", Document.new
+root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh"
+root["etc"].append "hosts", Document.new
+root["etc"]["hosts"].contents = "127.0.0.1 localhost"
+
+xml = root.save_to_xml 
+=> <folder name='home'> ... </>
+xml.write $stdout,2
+
+<folder name='home'>
+  <document name='plan'>
+    <contents>
+       inhale, exhale
+    </contents>
+  </document>
+  <folder name='work'>
+    <folder name='xml-mapping'>
+      <document name='README'>
+        <contents>
+          foo bar baz
+        </contents>
+      </document>
+    </folder>
+  </folder>
+  <folder name='etc'>
+    <document name='passwd'>
+      <contents>
+        foo:x:2:2:/bin/sh
+      </contents>
+    </document>
+    <document name='hosts'>
+      <contents>
+        127.0.0.1 localhost
+      </contents>
+    </document>
+  </folder>
+</folder>
+ +

As you see, the Folder#entries attribute is mapped via an +array_node that does not specify a :class or anything else to +govern the instantiation of the array's elements. This causes +xml-mapping to deduce the class of each array element from the root element +name of the corresponding XML tree. In this example, +the root element name is either “document” or “folder”. The mapping between +root element names and class names is the one briefly described in example at the beginning of this document – the +unqualified class name is just converted to lower case and “dashed”, e.g. +Foo::Bar::MyClass becomes “my-class”; and you may overwrite this on a +per-class basis by calling root_element_name +"the-new-name" in the class body. In our example, the root +element name “document” leads to an instantiation of class +Document, and the root element name “folder” leads to an +instantiation of class Folder.

+ +

Incidentally, the last example shows that you can readily derive mapping +classes from one another (as said before, you can also derive mapping +classes from other classes, include other modules into them etc. at will). +This works just like intuition thinks it should – when deriving one mapping +class from another one, the list of nodes in effect when loading/saving +instances of the derived class will consist of all nodes of that class and +all superclasses, starting with the topmost superclass that has nodes +defined. There is one thing to take care of though: When deriving mapping +classes from one another, you have to make sure to include +XML::Mapping in each class. This requirement exists purely due to +ease-of-implementation considerations; there are probably ways to do away +with it, but the inconvenience seemed not severe enough for me to bother +(as yet). Still, you might get “strange” errors if you forget to do it for +a class.

+ +

Besides the :class keyword argument and no argument, there is +a third way to specify the way the sub-objects are created from/marshalled +into their subtrees: :marshaller and/or +:unmarshaller keyword arguments. Here you pass procs in which +you just do all the work manually. So this is basically a “catch-all” for +cases where the other two alternatives are not appropriate for the problem +at hand. (TODO: Use other example?) Let's say we want to +extend the Signature class from the initial example to include +the date on which the signature was created. We want the new XML representation of such a signature to look like +this:

+ +
<Signature>
+  <Name>John Doe</Name>
+  <Position>product manager</Position>
+  <signed-on>
+    <day>13</day>
+    <month>2</month>
+    <year>2005</year>
+  </signed-on>
+</Signature>
+ +

So, a new “signed-on” element was added that holds the day, month, and +year. In the Signature instance in Ruby, we want the date to +be stored in an attribute named signed_on of type +Time (that's Ruby's built-in Time class).

+ +

One could think of using object_node, but something like +object_node :signed_on, "signed-on", :class=>Time +won't work because Time isn't a mapping class and +doesn't define methods load_from_xml and +fill_into_xml (we could easily define those though; we'll +talk about that possibility here and here). The fastest, most ad-hoc way to +achieve what we want are :marshaller and :unmarshaller keyword arguments, +like this:

+ +
require 'xml/mapping'
+require 'xml/xxpath_methods'
+
+class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+  object_node :signed_on, "signed-on",
+              :unmarshaller=>proc{|xml|
+                               y,m,d = [xml.first_xpath("year").text.to_i,
+                                        xml.first_xpath("month").text.to_i,
+                                        xml.first_xpath("day").text.to_i]
+                               Time.local(y,m,d)
+                             },
+              :marshaller=>proc{|xml,value|
+                             e = xml.elements.add; e.name = "year"; e.text = value.year
+                             e = xml.elements.add; e.name = "month"; e.text = value.month
+                             e = xml.elements.add; e.name = "day"; e.text = value.day
+
+                             # xml.first("year",:ensure_created=>true).text = value.year
+                             # xml.first("month",:ensure_created=>true).text = value.month
+                             # xml.first("day",:ensure_created=>true).text = value.day
+                           }
+end
+
+ +

The :unmarshaller proc will be called whenever a +Signature instance is being read in from an XML source. The xml argument passed to the +proc contains (as a REXML::Element +instance) the XML subtree corresponding to the +node's attribute's sub-object currently being read. In the case of +our object_node, the sub-object is just the node's +attribute (signed_on) itself, and the subtree is the one +rooted at the <signed-on> element (if this were e.g. an +array_node, the :unmarshaller proc would be +called once for each array element, and xml would hold the +subtree corresponding to the “current” array element). The proc is expected +to extract the sub-object's data from xml and return the +sub-object. So we have to read the “year”, “month”, and “day” elements, +construct a Time instance from them and return that. One could +just use the REXML API to do that, but I've +decided here to use the XPath interpreter that comes with xml-mapping +(xml/xxpath), and specifically the 'xml/xxpath_methods' utility +library that adds methods like first to REMXML::Element. We +call first on xml three times, passing XPath +expressions to extract the “year”/“month”/“day” sub-elements, construct the +Time instance from that and return it. The XPath library is +explained in more detail below.

+ +

The :marshaller proc will be called whenever a +Signature instance is being written into an XML tree. xml is again the XML subtree rooted at the <signed-on> element (it +will still be empty when this proc is called), and value is +the current value of the sub-object (again, since this is an +object_node, value is the node's attribute, +i.e. the Time instance). We have to fill xml with +the data from value here. So we add three elements “year”, +“month” and “day” and set their texts to the corresponding values from +value. The commented-out code shows an alternative +implementation of the same thing using the XPath interpreter.

+ +

It should be mentioned again that :marshaller/:unmarshaller procs are +possible with all single-attribute nodes with sub-objects, i.e. with +object_node, array_node, and +hash_node. So, if you wanted to map a whole array of date +values, you could use array_node with the same +:marshaller/:unmarshaller procs as above, for example:

+ +
array_node :birthdays, "birthdays", "birthday",
+           :unmarshaller=> <as above>,
+           :marshaller=> <as above>
+ +

You can see that :marshaller/:unmarshaller procs give you more flexibility, +but they also impose more work because you essentially have to do all the +work of marshalling/unmarshalling the sub-objects yourself. If you find +yourself copying and pasting marshaller/unmarshaller procs all over the +place, you should instead define your own node type or mix the +marshalling/unmarshalling capabilities into the Time class +itself. This is explained here and here, and you'll see that it's not +really much more work than writing :marshaller and :unmarshaller procs (you +essentially just move the code from those procs into your own node type +resp. into the Time class), so you should not hesitate to do +this.

+ +

Another thing worth mentioning is that you don't have to specify +both a :marshaller and an :unmarshaller simultaneously. You can as +well give only one of them, and in addition to that pass a +:class argument or no argument. When you do that, the +specified marshaller (or unmarshaller) will be used when marshalling (resp. +unmarshalling) the sub-objects, and the other passed argument +(:class or none) will be employed when unmarshalling (resp. +marshalling) the sub-objects. So, in effect, you can deactivate or +“short-cut” some part of the marshalling/unmarshalling functionality of a +node type while retaining another part.

+ +

Attribute Handling Details, Augmenting Existing Classes

+ +

I'll shed some more light on how single-attribute nodes add mapped +attributes to Ruby classes. An attribute declaration like

+ +
text_node :city, "City"
+
+ +

maps some portion of the XML tree (here: the “City” +sub-element) to an attribute (here: “city”) of the class whose body the +declaration appears in. When writing (marshalling) instances of the +surrounding class into an XML document, xml-mapping +will read the attribute value from the instance using the function named +city; when reading (unmarshalling) an instance from an XML document, xml-mapping will use the one-parameter +function city= to set the attribute in the instance to the +value read from the XML document.

+ +

If these functions don't exist at the time the node declaration is +executed, xml-mapping adds default implementations that simply read/write +the attribute value to instance variables that have the same name as the +attribute. For example, the city attribute declaration in the +Address class in the example added functions city +and city= that read/write from/to the instance variable +@city.

+ +

If, however, these functions already exist prior to defining the +attributes, xml-mapping will leave them untouched, so your precious +self-written accessor methods that do whatever complicated internal +processing of the data won't be overwritten.

+ +

This means that you can not only create new mapping classes from scratch, +you can also take existing classes that contain some “business logic” and +“augment” them with xml-mapping capabilities. As a simple example, +let's augment Ruby's “Time” class with node declarations that +declare XML mappings for the day, month etc. fields:

+ +
class Time
+  include XML::Mapping
+
+  numeric_node :year, "year"
+  numeric_node :month, "month"
+  numeric_node :day, "mday"
+  numeric_node :hour, "hours"
+  numeric_node :min, "minutes"
+  numeric_node :sec, "seconds"
+end
+
+
+nowxml=Time.now.save_to_xml 
+=> <time> ... </>
+nowxml.write($stdout,2)
+<time>
+  <year>
+    2015
+  </year>
+  <month>
+    3
+  </month>
+  <mday>
+    1
+  </mday>
+  <hours>
+    15
+  </hours>
+  <minutes>
+    31
+  </minutes>
+  <seconds>
+    6
+  </seconds>
+</time>
+ +

Here XML mappings are defined for the existing +fields year, month etc. Xml-mapping noticed that +the getter methods for those attributes existed, so it didn't overwrite +them. When calling save_to_xml on a Time object, +these methods are called and return the object's values for those +fields, which then get written to the output XML.

+ +

So you can convert Time objects into XML trees. What about reading them back in from XML? All XML reading operations +go through <Class>.load_from_xml. The +load_from_xml class method inherited from XML::Mapping (see XML::Mapping::ClassMethods#load_from_xml) +allocates a new instance of the class (Time), then calls +fill_from_xml (i.e. XML::Mapping#fill_from_xml) +on it. fill_from_xml iterates over all our nodes in the order +of their definition. For each node, its data (the <year>, or +<month>, or <day> etc. element) is read from the XML source and then written to the Time +instance via the respective setter method (year=, +month=, day= etc.). These methods didn't +exist in Time before (Time objects are +immutable), so xml-mapping defined its own, default setter methods that +just set @year, @month etc. This is of course +pretty useless because Time objects don't hold their time +in these variables, so the setter methods don't really change the time +of the Time object. So we have to redefine +load_from_xml for the Time class:

+ +
def Time.load_from_xml(xml, options={:mapping=>:_default})
+  year,month,day,hour,min,sec =
+    [xml.first_xpath("year").text.to_i,
+     xml.first_xpath("month").text.to_i,
+     xml.first_xpath("mday").text.to_i,
+     xml.first_xpath("hours").text.to_i,
+     xml.first_xpath("minutes").text.to_i,
+     xml.first_xpath("seconds").text.to_i]
+  Time.local(year,month,day,hour,min,sec)
+end
+
+ +

Other Nodes

+ +

All nodes I've shown so far (node types text_node, numeric_node, +boolean_node, object_node, array_node, and hash_node) were single-attribute +nodes: The first parameter to the node factory method of such a node is an +attribute name, and the attribute of that name is the only piece of the +state of instances of the node's mapping class that gets read/written +by the node.

+ +

choice_node

+ +

There is one node type distributed with xml-mapping that is not a +single-attribute node: choice_node. A choice_node +allows you to specify a sequence of pairs, each consisting of an XPath +expression and another node (any node is supported here, including other +choice_nodes). When reading in an XML source, the +choice_node will delegate the work to the first node in the sequence whose +corresponding XPath expression was matched in the XML. When writing an object back to XML, the choice_node will delegate the work to the +first node whose data was “present” in the object (for single-attribute +nodes, the data is considered “present” if the node's attribute is +non-nil; for choice_nodes, the data is considered “present” if at least one +of the node's sub-nodes is “present”).

+ +

As a (somewhat contrived) example, here's a mapping for +Publication objects that have either a single author +(contained in an “author” XML attribute) or several +“contributors” (contained in a sequence of “contr” XML elements):

+ +
class Publication
+  include XML::Mapping
+
+  choice_node :if,    '@author', :then, (text_node :author, '@author'),
+              :elsif, 'contr',   :then, (array_node :contributors, 'contr', :class=>String)
+end
+
+### usage
+
+p1 = Publication.load_from_xml(REXML::Document.new('<publication author="Jim"/>').root)
+=> #<Publication:0x007ff64a166a78 @author="Jim">
+
+p2 = Publication.load_from_xml(REXML::Document.new('
+<publication>
+  <contr>Chris</contr>
+  <contr>Mel</contr>
+  <contr>Toby</contr>
+</publication>').root)
+=> #<Publication:0x007ff64a155f48 @contributors=["Chris", "Mel", "Toby"]>
+ +

The symbols :if, :then, and :elsif (but not :else – see below) in the +choice_node's node factory method call are ignored; they +may be sprinkled across the argument list at will (preferably the way shown +above of course) to increase readability.

+ +

The rest of the arguments specify the mentioned sequence of XPath +expressions and corresponding nodes.

+ +

When reading a Publication object from XML, the XPath expressions from the +choice_node (@author and contr) will +be matched in sequence against the source XML tree +until a match is found or the end of the argument list is reached. If the +end is reached, an exception is raised. Otherwise, for the first XPath +expression that matched, the corresponding node will be invoked (i.e. used +to read actual data from the XML source into the +Person object). If you specify :else, :default, or :otherwise +in place of an XPath expression, this is treated as an XPath expression +that always matches. So you can use :else (or :default or :otherwise) for a +“fallback” node that will be used if none of the other XPath expressions +matched (an example for this follows).

+ +

When writing a Publication object back to XML, the first node in the sequence whose data is +“present” in the source object will be invoked to write data from the +object into the target XML tree (and the +corresponding XPath expression will be created in the XML tree if it doesn't exist already). If there is +no such node in the sequence, an exception is raised. As said above, for +single-attribute nodes, the node's data is considered “present” if the +node's attribute is non-nil. So, if you write a +Publication object to XML, and either +the author or the contributors attribute of the +object is set, it will be written; if both attributes are nil, an exception +will be raised.

+ +

A frequent use case for choice_nodes will probably be object attributes +that may be represented in multiple alternative ways in XML. As an example, consider “Person” objects where the +name of the person should be stored alternatively in a sub-element named +name, or an attribute named name, or in the text +of the person element itself. You can achieve this with +choice_node like this:

+ +
class Person
+  include XML::Mapping
+
+  choice_node :if,    'name',  :then, (text_node :name, 'name'),
+              :elsif, '@name', :then, (text_node :name, '@name'),
+              :else,  (text_node :name, '.')
+end
+
+### usage
+
+p1 = Person.load_from_xml(REXML::Document.new('<person name="Jim"/>').root)
+=> #<Person:0x007ff64a1cd660 @name="Jim">
+
+p2 = Person.load_from_xml(REXML::Document.new('<person><name>James</name></person>').root)
+=> #<Person:0x007ff64a1c54b0 @name="James">
+
+p3 = Person.load_from_xml(REXML::Document.new('<person>Suzy</person>').root)
+=> #<Person:0x007ff64a1b6820 @name="Suzy">
+
+
+p1.save_to_xml.write($stdout)
+<person><name>Jim</name></person>
+p2.save_to_xml.write($stdout)
+<person><name>James</name></person>
+p3.save_to_xml.write($stdout)
+<person><name>Suzy</name></person>
+ +

Here all sub-nodes of the choice_nodes are single-attribute nodes +(text_nodes) with the same attribute (name). As you see, when +writing persons to XML, the name is always stored in +a <name> sub-element. Of course, this is because that alternative +appears first in the choice_node.

+ +

Readers/Writers

+ +

Finally, all nodes support keyword arguments :reader and :writer +which allow you to extend or completely override the reading and/or writing +functionality of the node with your own code. The :reader as well as the +:writer argument must be a proc that takes as its arguments the Ruby object +to be read/written (instance of the mapping class the node belongs to) and +the XML tree to be written to/read from. An optional +third argument may be specified – it will receive a proc that wraps the +default reader/writer functionality of the node.

+ +

The :reader proc is for reading (from the XML into +the object), the :writer proc is for writing (from the object into the XML).

+ +

Here's a (really contrived) example:

+ +
class Foo
+  include XML::Mapping
+
+  text_node :name, "@name", :reader=>proc{|obj,xml,default_reader|
+                                       default_reader.call(obj,xml)
+                                       obj.name += xml.attributes['more']
+                                     },
+                            :writer=>proc{|obj,xml|
+                                       xml.attributes['bar'] = "hi #{obj.name} ho"
+                                     }
+end
+
+f = Foo.load_from_xml(REXML::Document.new('<foo name="Jim" more="XYZ"/>').root)
+=> #<Foo:0x007ff64a10e8c8 @name="JimXYZ">
+
+xml = f.save_to_xml 
+xml.write $stdout,2 
+<foo bar='hi JimXYZ ho'/>
+ +

So there's a “Foo” class with a text_node that would by default +(without the :reader and :writer proc) map the Ruby attribute “name” to the +XML attribute “name”. The :reader proc is invoked +when reading from XML into a Foo +object. The xml argument is the XML +tree, obj is the object. default_reader is the +proc that wraps the default reading functionality of the node. We invoke it +at the beginning. For this text_node, the default reading functionality is +to take the text of the “name” attribute of xml and put it +into the name attribute of obj. After that, we +take the text of the “more” attribute of xml and append it to +the name attribute of obj. So the XML tree <foo name="Jim" +more="XYZ"/> is converted to a Foo object +with name=“JimXYZ”.

+ +

In our :writer proc, we only take obj (the Foo +object to be written to XML) and xml +(the XML tree the stuff is to be written to). +Analogously to the :reader, we could take a proc that wraps the default +writing functionality of the node, but we don't do that here–we +completely override the writing functionality with our own code, which just +takes the name attribute of the object and writes “hi <the +name> ho” to a bar XML attribute in +the XML tree (stupid example, I know).

+ +

As a special convention, if you specify both a :reader and a :writer for a +node, and in both cases you do /not/ call the default behaviour, then you +should use the generic node type node, e.g.:

+ +
class SomeClass
+  include XML::Mapping
+
+  ....
+
+  node :reader=>proc{|obj,xml| ...},
+       :writer=>proc{|obj,xml| ...}
+end
+ +

(since you're completely replacing both the reading and the writing +functionality, you're effectively replacing all the functionality of +the node, so it would be pointless and confusing to use one of the more +“specific” node types)

+ +

As you see, the purpose of readers and writers is to make it possible to +augment or override a node's functionality arbitrarily, so there +shouldn't be anything that's absolutely impossible to achieve with +xml-mapping. However, if you use readers and writers without invoking the +default behaviour, you really do everything manually, so you're not +doing any less work than you would do if you weren't using xml-mapping +at all. So you'll probably use readers and/or writers for those bits of +your mapping semantics that can't be achieved with xml-mapping's +predefined node types (an alternative approach might be to override the +post_load and/or post_save instance methods on +the mapping class – see the reference documentation).

+ +

An advice similar to the one given above for marshallers/unmarshallers +applies here as well: If you find yourself writing lots of readers and +writers that only differ in some easily parameterizable aspects, you should +think about defining your own node types. We talk about that below, and it generally just means that you +move the (sensibly parameterized) code from your readers/writers to your +node types.

+ +

Multiple Mappings per Class

+ +

Sometimes you might want to represent the same Ruby object in multiple +alternative ways in XML. For example, the name of a +“Person” object could be represented either in a “name” element or a “name” +attribute.

+ +

xml-mapping supports this by allowing you to define multiple disjoint +“mappings” for a mapping class. A mapping is by convention identified with +a symbol, e.g. :my_mapping, :other_mapping etc., +and each mapping comprises a root element name and a set of node +definitions. In the body of a mapping class definition, you switch to +another mapping with use_mapping :the_mapping. All following +node declarations will be added to that mapping unless you specify +the option :mapping=>:another_mapping for a node declaration (all node +types support that option). The default mapping (the mapping used if there +was no previous use_mapping in the class body) is named +:_default.

+ +

All the worker methods like load_from_xml/file, +save_to_xml/file, load_object_from_xml/file +support a :mapping keyword argument to specify the mapping, +which again defaults to :_default.

+ +

In the following example, we define two mappings (the default one and a +mapping named :other) for Person objects with a +name, an age and an address:

+ +
require 'xml/mapping'
+
+class Address; end
+
+class Person
+  include XML::Mapping
+
+  # the default mapping. Stores the name and age in XML attributes,
+  # and the address in a sub-element "address".
+
+  text_node :name, "@name"
+  numeric_node :age, "@age"
+  object_node :address, "address", :class=>Address
+
+  use_mapping :other
+
+  # the ":other" mapping. Non-default root element name; name and age
+  # stored in XML elements; address stored in the person's element
+  # itself
+
+  root_element_name "individual"
+  text_node :name, "name"
+  numeric_node :age, "age"
+  object_node :address, ".", :class=>Address
+
+  # you could also specify the mapping on a per-node basis with the
+  # :mapping option, e.g.:
+  #
+  # numeric_node :age, "age", :mapping=>:other
+end
+
+
+class Address
+  include XML::Mapping
+
+  # the default mapping.
+
+  text_node :street, "street"
+  numeric_node :number, "number"
+  text_node :city, "city"
+  numeric_node :zip, "zip"
+
+  use_mapping :other
+
+  # the ":other" mapping.
+
+  text_node :street, "street-name"
+  numeric_node :number, "street-name/@number"
+  text_node :city, "city-name"
+  numeric_node :zip, "city-name/@zip-code"
+end
+
+
+### usage
+
+## XML representation of a person in the default mapping
+xml = REXML::Document.new('
+<person name="Suzy" age="28">
+  <address>
+    <street>Abbey Road</street>
+    <number>72</number>
+    <city>London</city>
+    <zip>18827</zip>
+  </address>
+</person>').root
+
+## load using the default mapping
+p = Person.load_from_xml xml 
+=> #<Person:0x007ff64a23e9c8 @name="Suzy", @age=28, @address=#<Address:0x007ff64a23d4b0 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
+
+## save using the default mapping
+xml2 = p.save_to_xml
+xml2.write $stdout,2 
+<person name='Suzy' age='28'>
+  <address>
+    <street>
+      Abbey Road
+    </street>
+    <number>
+      72
+    </number>
+    <city>
+      London
+    </city>
+    <zip>
+      18827
+    </zip>
+  </address>
+</person>
+## xml2 identical to xml
+
+
+## now, save the same person to XML using the :other mapping...
+other_xml = p.save_to_xml :mapping=>:other
+other_xml.write $stdout,2 
+<individual>
+  <name>
+    Suzy
+  </name>
+  <age>
+    28
+  </age>
+  <street-name number='72'>
+    Abbey Road
+  </street-name>
+  <city-name zip-code='18827'>
+    London
+  </city-name>
+</individual>
+## load it again using the :other mapping
+p2 = Person.load_from_xml other_xml, :mapping=>:other 
+=> #<Person:0x007ff64a20c838 @name="Suzy", @age=28, @address=#<Address:0x007ff64a2079a0 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
+
+## p2 identical to p
+ +

In this example, each of the two mappings contains nodes that map the same +set of Ruby attributes (name, age and address). This is probably what you +want most of the time (since you're normally defining multiple XML mappings for the same Ruby data), but it's not +a necessity at all. When a mapping class is defined, xml-mapping will add +all Ruby attributes from all mappings to it.

+ +

You may have noticed that the object_nodes in the +Person class apply the mapping they were themselves defined in +to their sub-ordinated class (Address). This is the case for +all Single-attribute Nodes with Sub-objects +(object_node, array_node and +hash_node) unless you explicitly specify a different mapping +for the sub-object(s) using the option :sub_mapping, e.g.

+ +
object_node :address, "address", :class=>Address, :sub_mapping=>:other
+
+ +

Defining your own Node Types

+ +

It's easy to write additional node types and register them with the +xml-mapping library (the following node types come with xml-mapping: +node, text_node, numeric_node, +boolean_node, object_node, +array_node, hash_node, choice_node).

+ +

I'll first show an example, then some more theoretical insight.

+ +

Example

+ +

Let's say we want to extend the Signature class from the +example to include the time at which the signature was created. We want the +new XML representation of such a signature to look +like this:

+ +
<Signature>
+  <Name>John Doe</Name>
+  <Position>product manager</Position>
+  <signed-on>
+    <day>13</day>
+    <month>2</month>
+    <year>2005</year>
+  </signed-on>
+</Signature>
+ +

(we only save year, month and day to make this example shorter), and the +mapping class declaration to look like this:

+ +
class Signature
+  include XML::Mapping
+
+  text_node :name, "Name"
+  text_node :position, "Position", :default_value=>"Some Employee"
+  time_node :signed_on, "signed-on", :default_value=>Time.now
+end
+
+ +

(i.e. a new “time_node” declaration was added).

+ +

We want this time_node call to define an attribute named +signed_on which holds the date value from the XML document in an instance of class Time.

+ +

This node type can be defined with this piece of code:

+ +
require 'xml/mapping/base'
+
+class TimeNode < XML::Mapping::SingleAttributeNode
+  def initialize(*args)
+    path,*args = super(*args)
+    @y_path = XML::XXPath.new(path+"/year")
+    @m_path = XML::XXPath.new(path+"/month")
+    @d_path = XML::XXPath.new(path+"/day")
+    args
+  end
+
+  def extract_attr_value(xml)
+    y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i,
+                                     @m_path.first(xml).text.to_i,
+                                     @d_path.first(xml).text.to_i]
+                                  }
+    Time.local(y,m,d)
+  end
+
+  def set_attr_value(xml, value)
+    @y_path.first(xml,:ensure_created=>true).text = value.year
+    @m_path.first(xml,:ensure_created=>true).text = value.month
+    @d_path.first(xml,:ensure_created=>true).text = value.day
+  end
+end
+
+
+XML::Mapping.add_node_class TimeNode
+
+ +

The last line registers the new node type with the xml-mapping library. The +name of the node factory method (“time_node”) is automatically derived from +the class name of the node type (“TimeNode”).

+ +

There will be one instance of the node type TimeNode per +time_node declaration per mapping class (not per mapping class +instance). That instance (the “node” for short) will be created by the node +factory method (time_node); there's no need to instantiate +the node type directly. The time_node method places the node +into the mapping class; the @owner attribute of the node is set to +reference the mapping class. The node factory method passes the mapping +class the node appears in (Signature), followed by its own +arguments, to the node's constructor. In the example, the +time_node method calls TimeNode.new(Signature, +:signed_on, "signed-on", :default_value=>Time.now)). +new of course creates the node and then delegates the +arguments to our initializer initialize. We first call the +superclass's initializer, which strips off from the argument list those +arguments it handles itself, and returns the remaining ones. In this case, +the superclass XML::Mapping::SingleAttributeNode +handles the Signature, :signed_on and +:default_value=>Time.now arguments – Signature +is stored into @owner, :signed_on is stored into +@attrname, and {:default_value=>Time.now} is +stored into @options. The remaining argument list +["signed-on"] is returned; we capture the +"signed-on" string in path (the rest of the +argument list (an empty array) we capture in args for returning it +at the end of the initializer. This isn't strictly necessary, it's +just a convention that a node class initializer should always return those +arguments it didn't handle itself). We'll interpret path +as an XPath expression that locates the time value relative to the parent +mapping object's XML tree (in this case, this +would be the XML tree rooted at the +<Signature> element, i.e. the tree the +Signature instance was read from). We'll later have to +read/store the year, month, and day values from +path+"/year", path+"/month", +and path+"/day", respectively, so we create (and +precompile) three corresponding XPath expressions using XML::XXPath.new and store them into +member variables of the node. XML::XXPath is +an XPath implementation that is bundled with xml-mapping. It is very +incomplete, but it supports writing (not just reading) of XML nodes, which is needed to support writing data back +to XML. The XML::XXPath library is explained in more detail +below.

+ +

The extract_attr_value method is called whenever an instance +of the mapping class the node belongs to (Signature in the +example) is being created from an XML tree. The +parameter xml is that tree (again, this is the tree rooted at the +<Signature> element in this example). The method +implementation is expected to extract the single attribute's value from +xml and return it, or raise XML::Mapping::SingleAttributeNode::NoAttrValueSet +if the attribute was “unset” in the XML (this +exception tells the framework that the default value should be put in place +if it was defined), or raise any other exception to signal an error and +abort the whole process. Our superclass XML::Mapping::SingleAttributeNode +will store the returned single attribute's value into the +signed_on attribute of the Signature instance +being read in. In our implementation, we apply the xpath expressions +created during initialization to xml (e.g. +@y_path.first(xml)). An expression +xpath_expr.first(xml) returns (as a REXML element) the first sub-element of xml +that matches xpath_expr, or raises XML::XXPathError if there was no such +element. We apply REXML's text method to the returned element +to get out the element's text, convert it to integer, and supply it to +the constructor of the Time object to be returned. As a side +note, if an XPath expression matches XML attributes, +XML::XXPath methods like first will +return XML::XXPath::Accessors::Attribute +nodes that behave similarly to REXML::Element nodes, including support for +messages like name and text, so this would've worked +also if our XPath expressions had referred to XML +attributes, not elements. The default_when_xpath_err thing +calls the supplied block and returns its value, but maps the exception XML::XXPathError to the mentioned XML::Mapping::SingleAttributeNode::NoAttrValueSet +(any other exceptions fall through unchanged). As said above, XML::Mapping::SingleAttributeNode::NoAttrValueSet +is caught by the framework (more precisely, by our superclass XML::Mapping::SingleAttributeNode), +and the default value is set if it was provided. So you should just wrap +default_when_xpath_err around any applications of XPath +expressions whose non-presence in the XML you want +to be considered a non-presence of the attribute you're trying to +extract. (XML::XXPath is designed to know knothing about XML::Mapping, so it doesn't raise XML::Mapping::SingleAttributeNode::NoAttrValueSet +directly)

+ +

The set_attr_value method is called whenever an instance of +the mapping class the node belongs to (Signature in the +example) is being stored into an XML tree. The +xml parameter is the XML tree (a REXML element node; here this is again the tree +rooted at the <Signature> element); value is +the current value of the single attribute (in this example, the +signed_on attribute of the Signature instance +being stored). xml will most probably be “half-populated” by the +time this method is called – the framework calls the +set_attr_value methods of all nodes of a mapping class in the +order of their definition, letting each node fill its “bit” into +xml. The method implementation is expected to write value +into (the correct sub-elements of) xml, or raise an exception to +signal an error and abort the whole process. No default value handling is +done here; set_attr_value won't be called at all if the +attribute had been set to its default value. In our implementation we grab +the year, month and day values from value (which must be a +Time), and store it into the sub-elements of xml +identified by XPath expressions @y_path, @m_path +and @d_path, respectively. We do this by calling XML::XXPath#first with an +additional parameter :ensure_created=>true. An expression +xpath_expr.first(xml,:ensure_created=>true) works just +like xpath_expr.first(xml) if xpath_expr was +already present in xml. If it was not, it is created (preferably +at the end of xml's list of sub-nodes), and returned. See below for a more detailed documentation of the XPath +interpreter.

+ +

Element order in created XML documents

+ +

As just said, XML::XXPath, when used to +create new XML nodes, generally appends those nodes +to the end of the list of subnodes of the node the xpath expression was +applied to. All xml-mapping nodes that come with xml-mapping use XML::XXPath when writing data to XML, and therefore also append their data to the XML data written by preceding nodes (the nodes are +invoked in the order of their definition). This means that, generally, your +output data will appear in the XML document in the +same order in which the corresponding xml-mapping node definitions appeared +in the mapping class (unless you used XPath expressions like foo which explicitly dictate a fixed position in the +sequence of XML nodes). For instance, in the +Order class from the example at the beginning of this +document, if we put the :signatures node before the +:items node, the <Signed-By> element will +appear before the sequence of <Item> elements +in the output XML.

+ +

The following is a more systematic overview of the basic node types. The +description is self-contained, so some information from the previous +section will be repeated.

+ +

Node Types Are Ruby Classes

+ +

A node type is implemented as a Ruby class derived from XML::Mapping::Node or one of its +subclasses.

+ +

The following node types (node classes) come with xml-mapping (they all +live in the XML::Mapping namespace, which +I've left out here for brevity):

+ +
Node
+ +-SingleAttributeNode
+ |  +-SubObjectBaseNode
+ |  |  +-ObjectNode
+ |  |  +-ArrayNode
+ |  |  +-HashNode
+ |  +-TextNode
+ |  +-NumericNode
+ |  +-BooleanNode
+ +-ChoiceNode
+ +

XML::Mapping::Node is the base class +for all nodes, XML::Mapping::SingleAttributeNode +is the base class for single-attribute nodes, +and XML::Mapping::SubObjectBaseNode +is the base class for single-attribute nodes +with sub-objects. XML::Mapping::TextNode, XML::Mapping::ArrayNode etc. are of +course the text_node, array_node etc. we've +talked about in this document. When you've written a new node class, +you register it with xml-mapping by calling +XML::Mapping.add_node_class MyNode. When you do that, +xml-mapping automatically defines the node factory method for your class – +the method's name (e.g. my_node) is derived from the +node's class name (e.g. Foo::Bar::MyNode) by stripping all parent +module names, and then converting capital letters to lowercase and +preceding them with an underscore. In fact, this is just how all the +predefined node types are defined – those node types are not “special”; +they're defined in the source file +xml/mapping/standard_nodes.rb and then registered normally in +xml/mapping.rb. The source code of the built-in nodes is not +very long or complicated; you may consider reading it in addition to this +text to gain a better understanding.

+ +

How Node Types Work

+ +

The xml-mapping core “operates” node types as follows:

+ +

Node Initialization

+ +

As said above, when a node class is registered with xml-mapping by calling +XML::Mapping.add_node_class TheNodeClass, xml-mapping +automatically generates the node factory method for that type. The node +factory method will effectively be defined as a class method of the XML::Mapping module, which is why one can call +it from the body of a mapping class definition. The generated method will +create a new instance of the node class (a node for short) by +calling new on the node class. The list of parameters to +new will consist of the mapping class, followed by all +arguments that were passed to the node factory method. For example, +when you have this node declaration:

+ +
class MyMappingClass
+  include XML::Mapping
+
+  my_node :foo, "bar", 42, :hi=>"ho"
+end
+
+ +

, then the node factory method (my_node) calls +MyNode.new(MyMappingClass, :foo, "bar", 42, +:hi=>"ho").

+ +

new of course creates the instance and calls initialize +on it. The initialize implementation will generally store the +parameters into some instance variables for later usage. As a convention, +initialize should always extract from the parameter list those +parameters it processes itself, process them, and return an array +containing the remaining (still unprocessed) parameters. Thus, an +implementation of initialize follows this pattern:

+ +
def initialize(*args)
+  myparam1,myparam2,...,myparamx,*args = super(*args)
+
+  .... process the myparam1,myparam2,...,myparamx ....
+
+  # return still unprocessed args
+  args
+end
+ +

(since the called superclass initializer is written the same way, the +parameter array returned by it will already be stripped of all parameters +that the superclass initializer (or any of its superclasses's +initializers) processed)

+ +

This technique is a simple way to “chain” the initializers of all +superclasses of a node class, starting with the topmost one (Node), so that +each initializer can easily find out and process the parameters it is +responsible for.

+ +

The base node class XML::Mapping::Node +provides an initialize implementation that, among other things +(described below), adds self (i.e. the created node) to the +internal list of nodes held by the mapping class, and sets the @owner +attribute of self to reference the mapping class.

+ +

So, effectively there will be one instance of a node class (a node) per +node definition, and that instance lives in the mapping class the node was +defined in.

+ +

Node Operation during Marshalling and Unmarshalling

+ +

When an instance of a mapping class is created or filled from an XML tree, xml-mapping will call xml_to_obj +on all nodes defined in that mapping class in the mapping the node is defined in, in the order of +their definition. Two parameters will be passed: the mapping class instance +being created/filled, and the XML tree the instance +is being created/filled from. The implementation of xml_to_obj +is expected to read whatever pieces of data it is responsible for from the +XML tree and put it into the appropriate +variables/attributes etc. of the instance.

+ +

When an instance of a mapping class is stored or filled into an XML tree, xml-mapping will call obj_to_xml +on all nodes defined in that mapping class in the mapping the node is defined in, in the order of +their definition, again passing as parameters the mapping class instance +being stored, and the XML tree the instance is being +stored/filled into. The implementation of obj_to_xml is +expected to read whatever pieces of data it is responsible for from the +instance and put it into the appropriate XML +elements/XML attr etc. of the XML tree.

+ +

Basic Node Types Overview

+ +

The following is an overview of how initialization and +marshalling/unmarshalling is implemented in the node base classes (Node, +SingleAttributeNode, and SubObjectBaseNode).

+ +

TODO: summary table: member var name; introduced in class; meaning

+ +

Node

+ +

In initialize, the mapping class and the option arguments are +stripped from the argument list. The mapping class is stored in @owner, the +option arguments are stored (as a hash) in @options (the hash will be empty +if no options were given). The mapping the node +is defined in is determined (:mapping option, last use_mapping +or :_default) and stored in @mapping. The node then stores +itself in the list of nodes of the mapping class belonging to the mapping +(@owner.xml_mapping_nodes(:mapping=>@mapping); see XML::Mapping::ClassMethods#xml_mapping_nodes). +This list is the list of nodes later used when marshalling/unmarshalling an +instance of the mapping class with respect to a given mapping. This means +that node implementors will not normally “see” anything of the mapping +(they don't need to access the @mapping variable) because the +marshalling/unmarshalling methods +(obj_to_xml/xml_to_obj) simply won't be +called if the node's mapping is not the same as the mapping the +marshalling/unmarshalling is happening with.

+ +

Furthermore, if :reader and/or :writer options were given, +xml_to_obj resp. obj_to_xml are transparently +overwritten on the node to delegate to the supplied :reader/:writer procs.

+ +

The marshalling/unmarshalling methods +(obj_to_xml/xml_to_obj) are not implemented in +Node (they just raise an exception).

+ +

SingleAttributeNode

+ +

In initialize, the attribute name is stripped from the argument +list and stored in @attrname, and an attribute of that name is added to the +mapping class the node belongs to.

+ +

During marshalling/unmarshalling of an object to/from XML, single-attribute nodes only read/write a single +piece of the object's state: the single attribute (@attrname) the node +handles. Because of this, the +obj_to_xml/xml_to_obj implementations in +SingleAttributeNode call two new methods introduced by SingleAttributeNode, +which must be overwritten by subclasses:

+ +
extract_attr_value(xml)
+
+set_attr_value(xml, value)
+
+ +

extract_attr_value(xml) is called by xml_to_obj +during unmarshalling. xml is the XML tree +being read. The method must read the attribute's value from +xml and return it. xml_to_obj will set the attribute +to that value.

+ +

set_attr_value(xml, value) is called by +obj_to_xml during marshalling. xml is the XML tree being written, value is the current +value of the attribute. The method must write value into (the +correct sub-elements/attributes) of xml.

+ +

SingleAttributeNode also handles the default value, if it was specified +(via the :default_value option): When writing data to XML, set_attr_value(xml, value) won't +be called if the attribute was set to the default value. When reading data +from XML, the extract_attr_value(xml) +implementation must raise a special exception, XML::Mapping::SingleAttributeNode::NoAttrValueSet, +if it wants to indicate that the data was not present in the XML. SingleAttributeNode will catch this exception and +put the default value, if it was defined, into the attribute.

+ +

SubObjectBaseNode

+ +

The initializer will set up additional member variables @sub_mapping, +@marshaller, and @unmarshaller.

+ +

@sub_mapping contains the mapping to be used when reading/writing the +sub-objects (either specified with :sub_mapping, or, by default, the +mapping the node itself was defined in).

+ +

@marshaller and @unmarshaller contain procs that encapsulate +writing/reading of sub-objects to/from XML, as +specified by the user with :class/:marshaller/:unmarshaller etc. options +(the meaning of those different options was described above). The procs are there to be called from +extract_attr_value or set_attr_value whenever the +need arises.

+ +

XPath Interpreter

+ +

XML::XXPath is an XPath parser. It is used in +xml-mapping node type definitions, but can just as well be utilized +stand-alone (it does not depend on xml-mapping). XML::XXPath is very incomplete and probably will +always be, but it should be reasonably efficient (XPath expressions are +precompiled), and, most importantly, it supports write access, which is +needed for writing objects to XML. For example, if +you create the path /foo/bar[3]/baz[@key='hiho'] in the XML document

+ +
<foo>
+  <bar>
+    <baz key="ab">hello</baz>
+    <baz key="xy">goodbye</baz>
+  </bar>
+</foo>
+ +

, you'll get:

+ +
<foo>
+  <bar>
+    <baz key='ab'>hello</baz>
+    <baz key='xy'>goodbye</baz>
+  </bar>
+  <bar/>
+  <bar>
+    <baz key='hiho'/>
+  </bar>
+</foo>
+ +

XML::XXPath is explained in more detail in +the reference documentation and the user_manual_xxpath file.

+ +

License

+ +

xml-mapping is licensed under the Apache License, version 2.0. See the +LICENSE file for details.

+
+ + + + + diff --git a/user_manual_xxpath.in.md b/user_manual_xxpath.in.md deleted file mode 100644 index d32f7bd..0000000 --- a/user_manual_xxpath.in.md +++ /dev/null @@ -1,201 +0,0 @@ -# XML-XXPATH - -## Overview, Motivation - -Xml-xxpath is an (incomplete) XPath interpreter that is at the moment -bundled with xml-mapping. It is built on top of REXML. xml-mapping -uses xml-xxpath extensively for implementing its node types -- see the -README file and the reference documentation (and the source code) for -details. xml-xxpath, however, does not depend on xml-mapping at all, -and is useful in its own right -- maybe I'll later distribute it as a -seperate library instead of bundling it. For the time being, if you -want to use this XPath implementation stand-alone, you can just rip -the files `lib/xml/xxpath.rb`, `lib/xml/xxpath/steps.rb`, and -`lib/xml/xxpath_methods.rb` out of the xml-mapping distribution and -use them on their own (they do not depend on anything else). - -xml-xxpath's XPath support is vastly incomplete (see below), but, in -addition to the normal reading/matching functionality found in other -XPath implementations (i.e. "find all elements in a given XML document -matching a given XPath expression"), xml-xxpath supports write -access. For example, when writing the XPath expression -`/foo/bar[3]/baz[@key='hiho']` to the XML document - - - - hello - goodbye - - - -, you'll get: - - - - hello - goodbye - - - - - -This feature is used by xml-mapping when writing (marshalling) Ruby -objects to XML, and is actually the reason why I couldn't just use any -of the existing XPath implementations, e.g. the one that comes with -REXML. Also, the whole xml-xxpath implementation is just 300 lines of -Ruby code, it is quite fast (paths are precompiled), and xml-xxpath -returns matched elements in the order they appeared in the source -document -- I've heard REXML::XPath doesn't do that :) - -Some basic knowledge of XPath is helpful for reading this document. - -At the moment, xml-xxpath understands XPath expressions of the form -[`/`]_pathelement_`/[/]`_pathelement_`/[/]`..., -where each _pathelement_ must be one of these: - -- a simple element name _name_, e.g. `signature` - -- an attribute name, @_attrname_, e.g. `@key` - -- a combination of an element name and an attribute name and - -value, in the form `elt_name[@attr_name='attr_value']` - -- an element name and an index, `elt_name[index]` - -- the "match-all" path element, `*` - -- . - -- name1`|`name2`|`... - -- `.[@key='xy'] / self::*[@key='xy']` - -- `child::*[@key='xy']` - -- `text()` - - - -Xml-xxpath only supports relative paths at this time, i.e. XPath -expressions beginning with "/" or "//" will still only find nodes -below the node the expression is applied to (as if you had written -"./" or ".//", respectively). - - -## Usage - -Xml-xxpath defines the class XML::XXPath. An instance of that class -wraps an XPath expression, the string representation of which must be -supplied when constructing the instance. You then call instance -methods like _first_, _all_ or create_new on the instance, -supplying the REXML Element the XPath expression should be applied to, -and get the results, or, in the case of write access, the element is -updated in-place. - - -### Read Access - - :include: xpath_usage.intout - -The objects supplied to the `all()`, `first()`, and -`each()` calls must be REXML element nodes, i.e. they must -support messages like `elements`, `attributes` etc -(instances of REXML::Element and its subclasses do this). The calls -return the found elements as instances of REXML::Element or -XML::XXPath::Accessors::Attribute. The latter is a wrapper around -attribute nodes that is largely call-compatible to -REXML::Element. This is so you can write things like -`path.each{|node|puts node.text}` without having to -special-case anything even if the path matches attributes, not just -elements. - -As you can see, you can re-use path objects, applying them to -different XML elements at will. You should do this because the XPath -pattern is stored inside the XPath object in a pre-compiled form, -which makes it more efficient. - -The path elements of the XPath pattern are applied to the -`.elements` collection of the passed XML element and its -sub-elements, starting with the first one. This is shown by the -following code: - - :include: xpath_docvsroot.intout - -A REXML +Document+ object is a REXML +Element+ object whose +elements+ -collection consists only of a single member -- the document's root -node. The first path element of the XPath -- "foo" in the example -- -is matched against that. That is why the path "/bar" in the example -doesn't match anything when matched against the document +d+ itself. - -An ordinary REXML +Element+ object that represents a node somewhere -inside an XML tree has an +elements+ collection that consists of all -the element's direct sub-elements. That is why XPath patterns matched -against the +firstelt+ element in the example *must not* start with -"/first" (unless there is a child node that is also named "first"). - - -### Write Access - -You may pass an `:ensure_created=>true` option argument to -_path_.first(_elt_) / _path_.all(_elt_) calls to make sure that _path_ -exists inside the passed XML element _elt_. If it existed before, -nothing changes, and the call behaves just as it would without the -option argument. If the path didn't exist before, the XML element is -modified such that - -- the path exists afterwards - -- all paths that existed before still exist afterwards - -- the modification is as small as possible (i.e. as few elements as - possible are added, additional attributes are added to existing - elements if possible etc.) - -The created resp. previously existing, matching elements are returned. - - -Examples: - - :include: xpath_ensure_created.intout - - -Alternatively, you may pass a `:create_new=>true` option -argument or call `create_new` (_path_`.create_new(`_elt_`)` is -equivalent to _path_`.first(`_elt_`,:create_new=>true)`). In that -case, a new node is created in _elt_ for each path element of _path_ -(or an exception raised if that wasn't possible for any path element). - -Examples: - - :include: xpath_create_new.intout - -This feature is used in xml-mapping by node types like -XML::Mapping::ArrayNode, which must create a new instance of the -"per-array element path" for each element of the array to be stored in -an XML tree. - - -### Pathological Cases - -What is created when the Path "*" is to be created inside an empty XML -element? The name of the element to be created isn't known, but still -some element must be created. The answer is that xml-xxpath creates a -special "unspecified" element whose name must be set by the caller -afterwards: - - :include: xpath_pathological.intout - -The "newelt" object in the last example is an ordinary -REXML::Element. xml-xxpath mixes the "unspecified" attribute into that -class, as well as into the XML::XXPath::Accessors::Attribute class -mentioned above. - - -## Implentation notes - -`doc/xpath_impl_notes.txt` contains some documentation on the -implementation of xml-xxpath. - -## License - -Ruby's. diff --git a/user_manual_xxpath_md.html b/user_manual_xxpath_md.html new file mode 100644 index 0000000..5bdedfa --- /dev/null +++ b/user_manual_xxpath_md.html @@ -0,0 +1,774 @@ + + + + + + +user_manual_xxpath - XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper + + + + + + + + + + + + + + +
+ +

XML-XXPATH

+ +

Overview, Motivation

+ +

Xml-xxpath is an (incomplete) XPath interpreter that is at the moment +bundled with xml-mapping. It is built on top of REXML. xml-mapping uses xml-xxpath extensively for +implementing its node types – see the README +file and the reference documentation (and the source code) for details. +xml-xxpath, however, does not depend on xml-mapping at all, and is useful +in its own right – maybe I'll later distribute it as a seperate library +instead of bundling it. For the time being, if you want to use this XPath +implementation stand-alone, you can just rip the files +lib/xml/xxpath.rb, lib/xml/xxpath/steps.rb, and +lib/xml/xxpath_methods.rb out of the xml-mapping distribution +and use them on their own (they do not depend on anything else).

+ +

xml-xxpath's XPath support is vastly incomplete (see below), but, in +addition to the normal reading/matching functionality found in other XPath +implementations (i.e. “find all elements in a given XML document matching a given XPath expression”), +xml-xxpath supports write access. For example, when writing the +XPath expression /foo/bar[3]/baz[@key='hiho'] to the XML document

+ +
<foo>
+  <bar>
+    <baz key='ab'>hello</baz>
+    <baz key='xy'>goodbye</baz>
+  </bar>
+</foo>
+ +

, you'll get:

+ +
<foo>
+  <bar>
+    <baz key='ab'>hello</baz>
+    <baz key='xy'>goodbye</baz>
+  </bar>
+  <bar/>
+  <bar><baz key='hiho'/></bar>
+</foo>
+ +

This feature is used by xml-mapping when writing (marshalling) Ruby objects +to XML, and is actually the reason why I +couldn't just use any of the existing XPath implementations, e.g. the +one that comes with REXML. Also, the whole +xml-xxpath implementation is just 300 lines of Ruby code, it is quite fast +(paths are precompiled), and xml-xxpath returns matched elements in the +order they appeared in the source document – I've heard REXML::XPath +doesn't do that :)

+ +

Some basic knowledge of XPath is helpful for reading this document.

+ +

At the moment, xml-xxpath understands XPath expressions of the form +[/]pathelement/pathelement/…, where each pathelement must be +one of these:

+
  • +

    a simple element name name, e.g. signature

    +
  • +

    an attribute name, @attrname, e.g. @key

    +
  • +

    a combination of an element name and an attribute name and -value, in the +form elt_name[@attr_name='attr_value']

    +
  • +

    an element name and an index, elt_name[index]

    +
  • +

    the "match-all" path element, *

    +
  • +

    .

    +
  • +

    name1|name2|...

    +
  • +

    .[@key='xy'] / self::*[@key='xy']

    +
  • +

    child::*[@key='xy']

    +
  • +

    text()

    +
+ +

Xml-xxpath only supports relative paths at this time, i.e. XPath +expressions beginning with “/” or “//” will still only find nodes below the +node the expression is applied to (as if you had written “./” or “.//”, +respectively).

+ +

Usage

+ +

Xml-xxpath defines the class XML::XXPath. An +instance of that class wraps an XPath expression, the string representation +of which must be supplied when constructing the instance. You then call +instance methods like first, all or create_new +on the instance, supplying the REXML Element the +XPath expression should be applied to, and get the results, or, in the case +of write access, the element is updated in-place.

+ +

Read Access

+ +
require 'xml/xxpath'
+
+d=REXML::Document.new <<EOS
+  <foo>
+    <bar>
+      <baz key="work">Java</baz>
+      <baz key="play">Ruby</baz>
+    </bar>
+    <bar>
+      <baz key="ab">hello</baz>
+      <baz key="play">scrabble</baz>
+      <baz key="xy">goodbye</baz>
+    </bar>
+    <more>
+      <baz key="play">poker</baz>
+    </more>
+  </foo>
+EOS
+
+####read access
+path=XML::XXPath.new("/foo/bar[2]/baz")
+
+## path.all(document) gives all elements matching path in document
+path.all(d)
+=> [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>]
+
+## loop over them
+path.each(d){|elt| puts elt.text}
+hello
+scrabble
+goodbye
+=> [<baz key='ab'> ... </>, <baz key='play'> ... </>, <baz key='xy'> ... </>]
+
+## the first of those
+path.first(d)
+=> <baz key='ab'> ... </>
+
+## no match here (only three "baz" elements)
+path2=XML::XXPath.new("/foo/bar[2]/baz[4]")
+path2.all(d)
+=> []
+
+## "first" raises XML::XXPathError in such cases...
+path2.first(d)
+XML::XXPathError: path not found: /foo/bar[2]/baz[4]
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:75:in `first'
+
+##...unless we allow nil returns
+path2.first(d,:allow_nil=>true)
+=> nil
+
+##attribute nodes can also be returned
+keysPath=XML::XXPath.new("/foo  /  @key")
+
+keysPath.all(d).map{|attr|attr.text}
+=> ["work", "play", "ab", "play", "xy", "play"]
+ +

The objects supplied to the all(), first(), and +each() calls must be REXML element +nodes, i.e. they must support messages like elements, +attributes etc (instances of REXML::Element and its subclasses do this). +The calls return the found elements as instances of REXML::Element or XML::XXPath::Accessors::Attribute. +The latter is a wrapper around attribute nodes that is largely +call-compatible to REXML::Element. This is +so you can write things like path.each{|node|puts node.text} +without having to special-case anything even if the path matches +attributes, not just elements.

+ +

As you can see, you can re-use path objects, applying them to different XML elements at will. You should do this because the +XPath pattern is stored inside the XPath object in a pre-compiled form, +which makes it more efficient.

+ +

The path elements of the XPath pattern are applied to the +.elements collection of the passed XML +element and its sub-elements, starting with the first one. This is shown by +the following code:

+ +
require 'xml/xxpath'
+
+d=REXML::Document.new <<EOS
+  <foo>
+    <bar x="hello">
+      <first>
+        <second>pingpong</second>
+      </first>
+    </bar>
+    <bar x="goodbye"/>
+  </foo>
+EOS
+
+XML::XXPath.new("/foo/bar").all(d)
+=> [<bar x='hello'> ... </>, <bar x='goodbye'/>]
+
+XML::XXPath.new("/bar").all(d)
+=> []
+
+XML::XXPath.new("/foo/bar").all(d.root)
+=> []
+
+XML::XXPath.new("/bar").all(d.root)
+=> [<bar x='hello'> ... </>, <bar x='goodbye'/>]
+
+firstelt = XML::XXPath.new("/foo/bar/first").first(d)
+=> <first> ... </>
+
+XML::XXPath.new("/first/second").all(firstelt)
+=> []
+
+XML::XXPath.new("/second").all(firstelt)
+=> [<second> ... </>]
+ +

A REXML Document object is a REXML Element object whose +elements collection consists only of a single member – the +document's root node. The first path element of the XPath – “foo” in +the example – is matched against that. That is why the path “/bar” in the +example doesn't match anything when matched against the document +d itself.

+ +

An ordinary REXML Element object that +represents a node somewhere inside an XML tree has +an elements collection that consists of all the element's +direct sub-elements. That is why XPath patterns matched against the +firstelt element in the example must not start with +“/first” (unless there is a child node that is also named “first”).

+ +

Write Access

+ +

You may pass an :ensure_created=>true option argument to +path.first(elt) / path.all(elt) calls +to make sure that path exists inside the passed XML element elt. If it existed before, nothing +changes, and the call behaves just as it would without the option argument. +If the path didn't exist before, the XML element +is modified such that

+
  • +

    the path exists afterwards

    +
  • +

    all paths that existed before still exist afterwards

    +
  • +

    the modification is as small as possible (i.e. as few elements as possible +are added, additional attributes are added to existing elements if +possible etc.)

    +
+ +

The created resp. previously existing, matching elements are returned.

+ +

Examples:

+ +
require 'xml/xxpath'
+
+d=REXML::Document.new <<EOS
+  <foo>
+    <bar>
+      <baz key="work">Java</baz>
+      <baz key="play">Ruby</baz>
+    </bar>
+  </foo>
+EOS
+
+rootelt=d.root
+
+#### ensuring that a specific path exists inside the document
+
+XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)
+=> <baz key='work'> ... </>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+</foo>### no change (path existed before)
+
+XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
+=> <baz key='42'/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+  </bar>
+</foo>### path was added
+
+XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
+=> <baz key='42'/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+  </bar>
+</foo>### no change this time
+
+XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)
+=> <baz key='work' key2='hello'> ... </>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='hello'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+  </bar>
+</foo>### this fit in the 1st "baz" element since
+### there was no "key2" attribute there before.
+
+XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)
+=> <baz key='play'> ... </>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='hello'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+  </bar>
+</foo>### no change
+
+XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
+=> #<XML::XXPath::Accessors::Attribute:0x007ff64a13ed48 @parent=<baz haha='[unset]'/>, @name="haha">
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='hello'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+    <baz/>
+    <baz/>
+    <baz haha='[unset]'/>
+  </bar>
+</foo>### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
+
+XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
+=> #<XML::XXPath::Accessors::Attribute:0x007ff64a12e240 @parent=<baz haha='[unset]'/>, @name="haha">
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='hello'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+    <baz key='42'/>
+    <baz/>
+    <baz/>
+    <baz haha='[unset]'/>
+  </bar>
+</foo>### no change this time
+ +

Alternatively, you may pass a :create_new=>true option +argument or call create_new +(path.create_new(elt) is +equivalent to +path.first(elt,:create_new=>true)). +In that case, a new node is created in elt for each path element +of path (or an exception raised if that wasn't possible for +any path element).

+ +

Examples:

+ +
require 'xml/xxpath'
+
+d=REXML::Document.new <<EOS
+  <foo>
+    <bar>
+      <baz key="work">Java</baz>
+      <baz key="play">Ruby</baz>
+    </bar>
+  </foo>
+EOS
+
+rootelt=d.root
+
+path1=XML::XXPath.new("/bar/baz[@key='work']")
+
+path1.create_new(rootelt)
+=> <baz key='work'/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+</foo>### a new element is created for *each* path element, regardless of
+### what existed before. So a new "bar" element was added, with a new
+### "baz" element inside it
+
+### same call again...
+path1.create_new(rootelt)
+=> <baz key='work'/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+</foo>### same procedure -- new elements added for each path element
+
+## get reference to 1st "baz" element
+firstbazelt=XML::XXPath.new("/bar/baz").first(rootelt)
+=> <baz key='work'> ... </>
+
+path2=XML::XXPath.new("@key2")
+
+path2.create_new(firstbazelt)
+=> #<XML::XXPath::Accessors::Attribute:0x007ff649a6bc00 @parent=<baz key='work' key2='[unset]'> ... </>, @name="key2">
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='[unset]'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+</foo>### ok, new attribute node added
+
+### same call again...
+path2.create_new(firstbazelt)
+XML::XXPathError: XPath (@key2): create_new and attribute already exists
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:215:in `create_on'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:80:in `block in creator'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `call'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `all'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:70:in `first'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:112:in `create_new'
+### can't create that path anew again -- an element can't have more
+### than one attribute with the same name
+
+### the document hasn't changed
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='[unset]'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+</foo>
+
+### create_new the same path as in the ensure_created example
+baz6elt=XML::XXPath.new("/bar/baz[6]").create_new(rootelt)
+=> <baz/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='[unset]'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+  </bar>
+</foo>### ok, new "bar" element and 6th "baz" element inside it created
+
+XML::XXPath.new("baz[6]").create_new(baz6elt.parent)
+XML::XXPathError: XPath: baz[6]: create_new and element already exists
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:167:in `create_on'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath/steps.rb:80:in `block in creator'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `call'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:91:in `all'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:70:in `first'
+    from /Users/oklischat/xml-mapping/lib/xml/xxpath.rb:112:in `create_new'
+### yep, baz[6] already existed and thus couldn't be created once
+### again
+
+### but of course...
+XML::XXPath.new("/bar/baz[6]").create_new(rootelt)
+=> <baz/>
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <baz key='work' key2='[unset]'>
+      Java
+    </baz>
+    <baz key='play'>
+      Ruby
+    </baz>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz key='work'/>
+  </bar>
+  <bar>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+  </bar>
+  <bar>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+    <baz/>
+  </bar>
+</foo>### this works because *all* path elements are newly created
+ +

This feature is used in xml-mapping by node types like XML::Mapping::ArrayNode, which must +create a new instance of the “per-array element path” for each element of +the array to be stored in an XML tree.

+ +

Pathological Cases

+ +

What is created when the Path “*” is to be created inside an empty XML element? The name of the element to be created +isn't known, but still some element must be created. The answer is that +xml-xxpath creates a special “unspecified” element whose name must be set +by the caller afterwards:

+ +
require 'xml/xxpath'
+
+d=REXML::Document.new <<EOS
+  <foo>
+    <bar/>
+    <bar/>
+  </foo>
+EOS
+
+rootelt=d.root
+
+XML::XXPath.new("*").all(rootelt)
+=> [<bar/>, <bar/>]
+### ok
+
+XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true)
+=> nil
+### ok, nothing there
+
+### the same call with :ensure_created=>true
+newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)
+=> </>
+
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    </>
+  </bar>
+  <bar/>
+</foo>
+### a new "unspecified" element was created
+newelt.unspecified?
+=> true
+
+### we must modify it to "specify" it
+newelt.name="new-one"
+newelt.text="hello!"
+newelt.unspecified?
+=> false
+
+d.write($stdout,2)
+
+<foo>
+  <bar>
+    <new-one>
+      hello!
+    </new-one>
+  </bar>
+  <bar/>
+</foo>
+### you could also set unspecified to false explicitly, as in:
+newelt.unspecified=true
+ +

The “newelt” object in the last example is an ordinary REXML::Element. xml-xxpath mixes the +“unspecified” attribute into that class, as well as into the XML::XXPath::Accessors::Attribute +class mentioned above.

+ +

Implentation notes

+ +

doc/xpath_impl_notes.txt contains some documentation on the +implementation of xml-xxpath.

+ +

License

+ +

Ruby's.

+
+ + + + +