├── .gitignore
├── LICENSE
├── README.md
├── Rakefile
├── TODO.txt
├── devel
├── runinteractive
└── xml_mapping_intinit.rb
├── doc
└── xpath_impl_notes.txt
├── examples
├── README
├── cleanup.rb
├── company.rb
├── company.xml
├── company_usage.intin.rb
├── documents_folders.rb
├── documents_folders.xml
├── documents_folders_usage.intin.rb
├── order.rb
├── order.xml
├── order_signature_enhanced.rb
├── order_signature_enhanced.xml
├── order_signature_enhanced_usage.intin.rb
├── order_usage.intin.rb
├── person.intin.rb
├── person_mm.intin.rb
├── publication.intin.rb
├── reader.intin.rb
├── stringarray.rb
├── stringarray.xml
├── stringarray_usage.intin.rb
├── time_augm.intin.rb
├── time_augm_loading.intin.rb
├── time_node.rb
├── time_node_w_marshallers.intin.rb
├── time_node_w_marshallers.xml
├── xpath_create_new.intin.rb
├── xpath_docvsroot.intin.rb
├── xpath_ensure_created.intin.rb
├── xpath_pathological.intin.rb
└── xpath_usage.intin.rb
├── lib
└── xml
│ ├── mapping.rb
│ ├── mapping
│ ├── base.rb
│ ├── core_classes_mapping.rb
│ ├── standard_nodes.rb
│ └── version.rb
│ ├── rexml_ext.rb
│ ├── xxpath.rb
│ ├── xxpath
│ └── steps.rb
│ └── xxpath_methods.rb
├── test
├── all_tests.rb
├── benchmark_fixtures.rb
├── bookmarks.rb
├── company.rb
├── documents_folders.rb
├── examples_test.rb
├── fixtures
│ ├── benchmark.xml
│ ├── bookmarks1.xml
│ ├── company1.xml
│ ├── documents_folders.xml
│ ├── documents_folders2.xml
│ ├── number.xml
│ ├── triangle_m1.xml
│ └── triangle_m2.xml
├── inheritance_test.rb
├── multiple_mappings_test.rb
├── number.rb
├── rexml_xpath_benchmark.rb
├── tests_init.rb
├── triangle_mm.rb
├── xml_mapping_adv_test.rb
├── xml_mapping_test.rb
├── xpath_test.rb
├── xxpath_benchmark.rb
└── xxpath_methods_test.rb
├── user_manual.in.md
└── user_manual_xxpath.in.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.intout
3 | *.lock
4 | doc/api
5 | user_manual.md
6 | user_manual_xxpath.md
7 | pkg/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 Olaf Klischat
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter
2 |
3 | Xml-mapping is an easy to use, extensible library that allows you to
4 | semi-automatically map Ruby objects to XML trees and vice versa.
5 |
6 |
7 | ## Trivial Example
8 |
9 | ### sample document
10 |
11 |
12 |
13 | -
14 | Stuffed Penguin
15 | 10
16 | 8.95
17 |
18 |
19 | ### mapping class
20 |
21 | class Item
22 | include XML::Mapping
23 |
24 | text_node :ref, "@reference"
25 | text_node :descr, "Description"
26 | numeric_node :quantity, "Quantity"
27 | numeric_node :unit_price, "UnitPrice"
28 |
29 | def total_price
30 | quantity*unit_price
31 | end
32 | end
33 |
34 |
35 | ### usage
36 |
37 | i = Item.load_from_file("item.xml")
38 | => #
39 |
40 | i.unit_price = 42.23
41 | xml=i.save_to_xml #convert to REXML node; there's also o.save_to_file(name)
42 | xml.write($stdout,2)
43 |
44 | -
45 | Stuffed Penguin
46 | 10
47 | 42.23
48 |
49 |
50 |
51 |
52 | This is the most trivial example -- the mapper supports arbitrary
53 | array and hash (map) nodes, object (reference) nodes and arrays/hashes
54 | of those, polymorphic mappings, multiple mappings per class, fully
55 | programmable mappings and arbitrary user-defined node types. Read the
56 | [project documentation](http://multi-io.github.io/xml-mapping/
57 | "Project Page") for more information.
58 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # -*- ruby -*-
2 | # adapted from active_record's Rakefile
3 |
4 | $:.unshift "."
5 |
6 | require 'rubygems'
7 | require 'rake'
8 | require 'rake/clean'
9 | require 'rake/testtask'
10 | require 'rdoc/task'
11 | require 'rake/packagetask'
12 | require 'rubygems/package_task'
13 |
14 | require File.dirname(__FILE__)+"/lib/xml/mapping/version"
15 |
16 | FILE_RDOC_MAIN = 'user_manual.md'
17 | FILES_RDOC_EXTRA = [FILE_RDOC_MAIN] + %w{README.md user_manual_xxpath.md TODO.txt doc/xpath_impl_notes.txt}
18 | FILES_RDOC_INCLUDES=`git ls-files examples`.split("\n").map{|f| f.gsub(/.intin.rb$/, '.intout')}
19 |
20 |
21 | desc "Default Task"
22 | task :default => [ :test ]
23 |
24 | Rake::TestTask.new :test do |t|
25 | t.test_files = ["test/all_tests.rb"]
26 | t.verbose = true
27 | # t.loader = :testrb
28 | end
29 |
30 | # runs tests only if sources have changed since last succesful run of
31 | # tests
32 | file "test_run" => FileList.new('lib/**/*.rb','test/**/*.rb') do
33 | Task[:test].invoke
34 | touch "test_run"
35 | end
36 |
37 |
38 |
39 | RDoc::Task.new do |rdoc|
40 | rdoc.rdoc_dir = 'doc/api'
41 | rdoc.title = "XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper"
42 | rdoc.options += %w{--line-numbers --include examples}
43 | rdoc.main = FILE_RDOC_MAIN
44 | rdoc.rdoc_files.include(*FILES_RDOC_EXTRA)
45 | rdoc.rdoc_files.include('lib/**/*.rb')
46 |
47 | task :rdoc => (FileList.new("examples/**/*.rb") + FILES_RDOC_INCLUDES)
48 | end
49 |
50 |
51 | ## need to process :include: statements manually so we can
52 | ## have the resulting markdown in the gem
53 | ### can't use a rule (recursion issues)
54 | %w{user_manual.md user_manual_xxpath.md}.each do |out_name|
55 | in_name = "#{File.basename(out_name,'.md')}.in.md"
56 | CLEAN << out_name
57 | file out_name => in_name do
58 | begin
59 | File.open(out_name, "w") do |fout|
60 | File.open(in_name, "r") do |fin|
61 | fin.each_line do |l|
62 | if m=l.match(/:include: (.*)/)
63 | File.open("examples/#{m[1]}") do |fincluded|
64 | fincluded.each_line do |linc|
65 | fout.puts " #{linc}"
66 | end
67 | end
68 | else
69 | fout.write l
70 | end
71 | end
72 | end
73 | end
74 | rescue Exception
75 | File.delete out_name
76 | raise
77 | end
78 | end
79 | end
80 |
81 |
82 | #rule '.intout' => ['.intin.rb', *FileList.new("lib/**/*.rb")] do |task| # doesn't work -- see below
83 | rule '.intout' => ['.intin.rb'] do |task|
84 | this_file_re = Regexp.compile(Regexp.quote(__FILE__))
85 | b = binding
86 | visible=true; visible_retval=true; handle_exceptions=false
87 | old_stdout = $stdout
88 | old_wd = Dir.pwd
89 | begin
90 | File.open(task.name,"w") do |fout|
91 | $stdout = fout
92 | File.open(task.source,"r") do |fin|
93 | Dir.chdir File.dirname(task.name)
94 | fin.read.split("#<=\n").each do |snippet|
95 |
96 | snippet.scan(/^#:(.*?):$/) do |switches|
97 | case switches[0]
98 | when "visible"
99 | visible=true
100 | when "invisible"
101 | visible=false
102 | when "visible_retval"
103 | visible_retval=true
104 | when "invisible_retval"
105 | visible_retval=false
106 | when "handle_exceptions"
107 | handle_exceptions=true
108 | when "no_exceptions"
109 | handle_exceptions=false
110 | end
111 | end
112 | snippet.gsub!(/^#:.*?:(?:\n|\z)/,'')
113 |
114 | print "#{snippet}\n" if visible
115 | exc_handled = false
116 | value = begin
117 | eval(snippet,b)
118 | rescue Exception
119 | raise unless handle_exceptions
120 | exc_handled = true
121 | if visible
122 | print "#{$!.class}: #{$!}\n"
123 | for m in $@
124 | break if m=~this_file_re
125 | print "\tfrom #{m}\n"
126 | end
127 | end
128 | end
129 | if visible and visible_retval and not exc_handled
130 | print "=> #{value.inspect}\n"
131 | end
132 | end
133 | end
134 | end
135 | rescue Exception
136 | $stdout = old_stdout
137 | Dir.chdir old_wd
138 | File.delete task.name
139 | raise
140 | ensure
141 | $stdout = old_stdout
142 | Dir.chdir old_wd
143 | end
144 | end
145 |
146 | # have to add additional prerequisites manually because it appears
147 | # that rules can only define a single prerequisite :-\
148 | FILES_RDOC_INCLUDES.select{|f|f=~/.intout$/}.each do |f|
149 | CLEAN << f
150 | file f => FileList.new("lib/**/*.rb")
151 | file f => FileList.new("examples/**/*.rb")
152 | end
153 |
154 | file 'examples/company_usage.intout' => ['examples/company.xml']
155 | file 'examples/documents_folders_usage.intout' => ['examples/documents_folders.xml']
156 | file 'examples/order_signature_enhanced_usage.intout' => ['examples/order_signature_enhanced.xml']
157 | file 'examples/order_usage.intout' => ['examples/order.xml']
158 | file 'examples/stringarray_usage.intout' => ['examples/stringarray.xml']
159 |
160 |
161 | spec = Gem::Specification.new do |s|
162 | s.name = 'xml-mapping'
163 | s.version = XML::Mapping::VERSION
164 | s.platform = Gem::Platform::RUBY
165 | s.summary = "XML-Object mapper for Ruby"
166 | s.description =
167 | "An easy to use, extensible library for semi-automatically mapping Ruby objects to XML and back. Includes an XPath interpreter."
168 | s.files += FILES_RDOC_EXTRA
169 | s.files += FILES_RDOC_INCLUDES
170 | s.files += `git ls-files lib test`.split("\n")
171 | s.files += %w{LICENSE Rakefile}
172 | s.extra_rdoc_files = FILES_RDOC_EXTRA
173 | s.rdoc_options += %w{--include examples}
174 | s.require_path = 'lib'
175 | s.add_development_dependency 'rake', '~> 0'
176 | s.test_file = 'test/all_tests.rb'
177 | s.author = 'Olaf Klischat'
178 | s.email = 'olaf.klischat@gmail.com'
179 | s.homepage = "https://github.com/multi-io/xml-mapping"
180 | s.rubyforge_project = "xml-mapping"
181 | s.licenses = ['Apache-2.0']
182 | end
183 |
184 |
185 |
186 | Gem::PackageTask.new(spec) do |p|
187 | p.gem_spec = spec
188 | p.need_tar = true
189 | p.need_zip = true
190 | end
191 |
192 |
193 |
194 | require 'tmpdir'
195 |
196 | def system_checked(*args)
197 | system(*args) or raise "failed to run: #{args.inspect}"
198 | end
199 |
200 | desc "updates gh-pages branch in the git with the latest rdoc"
201 | task :ghpublish => [:rdoc] do
202 | revision = `git rev-parse HEAD`.chomp
203 | Dir.mktmpdir do |dir|
204 | # --no-checkout also deletes all files in the target's index
205 | system_checked("git clone --branch gh-pages --no-checkout . #{dir}")
206 | cp_r FileList.new('doc/api/*'), dir
207 | system_checked("cd #{dir} && git add . && git commit -m 'upgrade to #{revision}'")
208 | system_checked("git fetch #{dir}")
209 | system_checked("git branch -f gh-pages FETCH_HEAD")
210 | end
211 | end
212 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | - consider switching from REXML to nokogiri and/or, maybe, ox.
2 |
3 | - XML::XXPath: Write a real XPath parser eventually
4 |
5 | - XML::XXPath: avoid duplicates in path.all(node) result arrays when
6 | using the descendants ("//") axis
7 |
8 | - invent an XPath-like language for Ruby object graphs (i.e. a
9 | language that is to Ruby object graphs what XPath is to XML
10 | trees). Use expressions in that language as a generalization of
11 | "attribute names" (e.g. the 1st parameter to single attribute node
12 | factory methods). The language could more or less be Ruby itself,
13 | but the write support would need some extra work...
14 |
15 | - XML::XXPath:
16 |
17 | - implement .[@attrname] steps
18 |
19 | - returns the context node iff it contains an attrname attribute
20 |
21 | - doesn't work properly in REXML::XPath?
22 |
23 | - implement *[@attrname] steps
24 |
25 | - implement *[@attrname='attrvalue'] steps
26 |
27 | - id/idref support (write support possible?)
28 |
29 | - XML::Mapping: make SubObjectBaseNode a mixin instead of a subclass
30 | of SingleAttributeNode ("mapping sub-objects" and "mapping to a
31 | single attribute" are orthogonal concepts; inheritance is bad design
32 | here)
33 |
34 | - documentation:
35 |
36 | - consider switching to YARD
37 |
38 | - reasons: parameter/return type metadata, (maybe) plugin for the
39 | code snippet inclusion stuff
40 |
41 | - user_manual:
42 |
43 | - document/show usage of default_when_xpath_err outside node type
44 | implementations
45 |
46 | - user_manual_xxpath:
47 |
48 | - mention new step types, new axes, xml/xpath_methods
49 |
50 |
51 | - XML::XXPath/XML::Mapping: support for XML namespaces in XML::XXPath
52 | (match nodes with specific namespaces only) and XML::Mapping
53 | (use_namespace etc.)
54 |
55 | - add streaming input/output to XML::Mapping, i.e. SAX-based input in
56 | addition to the current REXML/DOM - based one. Probably won't be
57 | implementable for some more complicated XPaths -- raise meaningful
58 | exceptions in those cases.
59 |
60 | - would need support in xxpath
61 |
62 | - should probably be built on top of the Ruby 2.0 lazy enumeration
63 | stuff
64 |
65 | - XML::XXPath/XML::Mapping: add XML text nodes (the sub-node of an
66 | element node that contains that element's text) first-class to
67 | XML::XXPath. Use it for things like text_node :contents, "text()".
68 |
69 | Along those lines: promote XPath node "unspecifiedness" from an
70 | attribute to a REXML node object of "unspecified" class that's
71 | turned into an attribute/element/text node when necessary
72 |
73 | - (eventually, maybe) provide a "scaffolding" feature to automatically
74 | turn a dtd/schema into a set of node type definitions or even a set
75 | of mapping classes
76 |
--------------------------------------------------------------------------------
/devel/runinteractive:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export RAILS_ENV=test
4 |
5 | if [ -n "$1" ]; then
6 | require_cmdline_lib="require \"$1\""
7 | fi
8 |
9 | ruby -r irb -e "
10 | $:.unshift \"../lib\"
11 | $:.unshift \"../test\"
12 |
13 | $require_cmdline_lib
14 | IRB.start
15 | "
16 |
--------------------------------------------------------------------------------
/devel/xml_mapping_intinit.rb:
--------------------------------------------------------------------------------
1 | require 'company'
2 | @xml = REXML::Document.new(File.new("../test/fixtures/company1.xml"))
3 | @c = Company.load_from_xml(@xml.root)
4 |
5 |
6 | # REXML::XPath is missing all()...
7 | def xpathall(path,xml)
8 | r=[]
9 | XPath.each(xml,path){|x|r<//.../ is
14 | compiled into a bunch of nested closures, each of which is responsible
15 | for a specific path element and calls the corresponding accessor
16 | function:
17 |
18 | - @creator_procs -- an array of "creator"
19 | functions. @creator_procs[i] gets passed a base node (XML
20 | element) and a create_new flag, and it creates the path
21 | //.../ inside the
22 | base node and returns the hindmost element created (i.e. the one
23 | corresponding to ).
24 |
25 | - @reader_proc -- a "reader" function that gets passed an
26 | array of nodes and returns an array of all nodes that matched the
27 | path in any of the supplied nodes, or, if no match was found, throws
28 | :not_found along with the last non-empty set of nodes that was
29 | found, and the element of @creator_procs that could be used
30 | to create the remaining part of the path.
31 |
32 | The +all+ function is then trivially implemented on top of this:
33 |
34 | def all(node,options={})
35 | raise "options not a hash" unless Hash===options
36 | if options[:create_new]
37 | return [ @creator_procs[-1].call(node,true) ]
38 | else
39 | last_nodes,rest_creator = catch(:not_found) do
40 | return @reader_proc.call([node])
41 | end
42 | if options[:ensure_created]
43 | [ rest_creator.call(last_nodes[0],false) ]
44 | else
45 | []
46 | end
47 | end
48 | end
49 |
50 | ...and +first+, create_new etc. are even more trivial
51 | frontends to that.
52 |
53 | The implementations of the @creator_procs look like this:
54 |
55 | @creator_procs[0] =
56 | proc{|node,create_new| node}
57 |
58 | @creator_procs[1] =
59 | proc {|node,create_new|
60 | @creator_procs[0].call(Accessors.create_subnode_by_(node,create_new,),
61 | create_new)
62 | }
63 |
64 | @creator_procs[2] =
65 | proc {|node,create_new|
66 | @creator_procs[1].call(Accessors.create_subnode_by_(node,create_new,),
67 | create_new)
68 | }
69 |
70 | ...
71 |
72 | @creator_procs[n] =
73 | proc {|node,create_new|
74 | @creator_procs[n-1].call(Accessors.create_subnode_by_(node,create_new,),
75 | create_new)
76 | }
77 |
78 | ...
79 | @creator_procs[x] =
80 | proc {|node,create_new|
81 | @creator_procs[x-1].call(Accessors.create_subnode_by_(node,create_new,),
82 | create_new)
83 | }
84 |
85 |
86 |
87 | ..and the implementation of @reader_proc looks like this:
88 |
89 | @reader_proc = rpx where
90 |
91 | rp0 = proc {|nodes| nodes}
92 |
93 | rp1 = proc {|nodes|
94 | next_nodes = Accessors.subnodes_by_(nodes,)
95 | if (next_nodes == [])
96 | throw :not_found, [nodes,@creator_procs[1]]
97 | else
98 | rp0.call(next_nodes)
99 | end
100 | }
101 |
102 | rp2 = proc {|nodes|
103 | next_nodes = Accessors.subnodes_by_(nodes,)
104 | if (next_nodes == [])
105 | throw :not_found, [nodes,@creator_procs[2]]
106 | else
107 | rp1.call(next_nodes)
108 | end
109 | }
110 | ...
111 |
112 | rpx = proc {|nodes|
113 | next_nodes = Accessors.subnodes_by_(nodes,)
114 | if (next_nodes == [])
115 | throw :not_found, [nodes,@creator_procs[x]]
116 | else
117 | rpx-1.call(next_nodes)
118 | end
119 | }
120 |
--------------------------------------------------------------------------------
/examples/README:
--------------------------------------------------------------------------------
1 | This directory contains the example code snippets from the user manual
2 | file (each time the documentation is regenerated, they're included in
3 | the user manual, executed, and their output is checked for correctness
4 | and included in the manual as well). So, if you've read the user
5 | manual, you've already seen all the examples from this directory.
6 |
--------------------------------------------------------------------------------
/examples/cleanup.rb:
--------------------------------------------------------------------------------
1 | %w{Address Client Company Customer Document Entry Folder Foo Item Order People Person Publication Signature}.each do |cname|
2 | begin
3 | Object.send(:remove_const, cname) # name clash with company_usage...
4 | rescue
5 | end
6 | end
7 |
8 |
9 | %w{company documents_folders order order_signature_enhanced stringarray time_node}.each do |mod|
10 | $".delete_if{|f| f =~ %r{/#{mod}.rb$} }
11 | end
12 |
--------------------------------------------------------------------------------
/examples/company.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | ## forward declarations
4 | class Address; end
5 | class Customer; end
6 |
7 |
8 | class Company
9 | include XML::Mapping
10 |
11 | text_node :name, "@name"
12 | object_node :address, "address", :class=>Address
13 | array_node :customers, "customers", "customer", :class=>Customer
14 | end
15 |
16 |
17 | class Address
18 | include XML::Mapping
19 |
20 | text_node :city, "city"
21 | numeric_node :zip, "zip"
22 | end
23 |
24 |
25 | class Customer
26 | include XML::Mapping
27 |
28 | text_node :id, "@id"
29 | text_node :name, "name"
30 |
31 | def initialize(id,name)
32 | @id,@name = [id,name]
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/examples/company.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Berlin
7 | 10113
8 |
9 |
10 |
11 |
12 |
13 | James Kirk
14 |
15 |
16 |
17 | Ernie
18 |
19 |
20 |
21 | Bert
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/company_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 |
4 | load "cleanup.rb"
5 |
6 | require 'company' #<=
7 | #:visible:
8 | c = Company.load_from_file('company.xml') #<=
9 | c.name #<=
10 | c.customers.size #<=
11 | c.customers[1] #<=
12 | c.customers[1].name #<=
13 | c.customers[0].name #<=
14 | c.customers[0].name = 'James Tiberius Kirk' #<=
15 | c.customers << Customer.new('cm','Cookie Monster') #<=
16 | xml2 = c.save_to_xml #<=
17 | #:invisible_retval:
18 | xml2.write($stdout,2) #<=
19 |
--------------------------------------------------------------------------------
/examples/documents_folders.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | class Entry
4 | include XML::Mapping
5 |
6 | text_node :name, "@name"
7 | end
8 |
9 |
10 | class Document []
21 |
22 | def [](name)
23 | entries.select{|e|e.name==name}[0]
24 | end
25 |
26 | def append(name,entry)
27 | entries << entry
28 | entry.name = name
29 | entry
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/examples/documents_folders.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | inhale, exhale
6 |
7 |
8 |
9 |
10 |
11 | foo bar baz
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/documents_folders_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'documents_folders' #<=
4 | #:visible:
5 |
6 | root = XML::Mapping.load_object_from_file "documents_folders.xml" #<=
7 | root.name #<=
8 | root.entries #<=
9 |
10 | root.append "etc", Folder.new
11 | root["etc"].append "passwd", Document.new
12 | root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh"
13 | root["etc"].append "hosts", Document.new
14 | root["etc"]["hosts"].contents = "127.0.0.1 localhost"
15 |
16 | xml = root.save_to_xml #<=
17 | #:invisible_retval:
18 | xml.write $stdout,2
19 |
--------------------------------------------------------------------------------
/examples/order.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | ## forward declarations
4 | class Client; end
5 | class Address; end
6 | class Item; end
7 | class Signature; end
8 |
9 |
10 | class Order
11 | include XML::Mapping
12 |
13 | text_node :reference, "@reference"
14 | object_node :client, "Client", :class=>Client
15 | hash_node :items, "Item", "@reference", :class=>Item
16 | array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
17 |
18 | def total_price
19 | items.values.map{|i| i.total_price}.inject(0){|x,y|x+y}
20 | end
21 | end
22 |
23 |
24 | class Client
25 | include XML::Mapping
26 |
27 | text_node :name, "Name"
28 | object_node :home_address, "Address[@where='home']", :class=>Address
29 | object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil
30 | end
31 |
32 |
33 | class Address
34 | include XML::Mapping
35 |
36 | text_node :city, "City"
37 | text_node :state, "State"
38 | numeric_node :zip, "ZIP"
39 | text_node :street, "Street"
40 | end
41 |
42 |
43 | class Item
44 | include XML::Mapping
45 |
46 | text_node :descr, "Description"
47 | numeric_node :quantity, "Quantity"
48 | numeric_node :unit_price, "UnitPrice"
49 |
50 | def total_price
51 | quantity*unit_price
52 | end
53 | end
54 |
55 |
56 | class Signature
57 | include XML::Mapping
58 |
59 | text_node :name, "Name"
60 | text_node :position, "Position", :default_value=>"Some Employee"
61 | end
62 |
--------------------------------------------------------------------------------
/examples/order.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Jean Smith
6 |
7 | San Mateo
8 | CA
9 | 94403
10 | 2000, Alameda de las Pulgas
11 |
12 |
13 | San Francisco
14 | CA
15 | 94102
16 | 98765, Fulton Street
17 |
18 |
19 |
20 | -
21 | Stuffed Penguin
22 | 10
23 | 8.95
24 |
25 |
26 | -
27 | Chocolate
28 | 5
29 | 28.50
30 |
31 |
32 | -
33 | Cookie
34 | 30
35 | 0.85
36 |
37 |
38 |
39 |
40 | John Doe
41 | product manager
42 |
43 |
44 |
45 | Jill Smith
46 | clerk
47 |
48 |
49 |
50 | Miles O'Brien
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/examples/order_signature_enhanced.rb:
--------------------------------------------------------------------------------
1 | class Signature
2 | include XML::Mapping
3 |
4 | text_node :name, "Name"
5 | text_node :position, "Position", :default_value=>"Some Employee"
6 | time_node :signed_on, "signed-on", :default_value=>Time.now
7 | end
8 |
--------------------------------------------------------------------------------
/examples/order_signature_enhanced.xml:
--------------------------------------------------------------------------------
1 |
2 | John Doe
3 | product manager
4 |
5 | 13
6 | 2
7 | 2005
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/order_signature_enhanced_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'xml/mapping'
4 | require 'time_node.rb'
5 | require 'order'
6 | require 'order_signature_enhanced'
7 | #<=
8 | #:visible:
9 | s=Signature.load_from_file("order_signature_enhanced.xml") #<=
10 | s.signed_on #<=
11 | s.signed_on=Time.local(1976,12,18) #<=
12 | s.save_to_xml.write($stdout,2) #<=
13 |
--------------------------------------------------------------------------------
/examples/order_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | load "cleanup.rb"
4 |
5 | require 'order'
6 |
7 | require 'xml/xxpath_methods'
8 |
9 | require 'test/unit/assertions'
10 | include Test::Unit::Assertions #<=
11 | #:visible:
12 | ####read access
13 | o=Order.load_from_file("order.xml") #<=
14 | o.reference #<=
15 | o.client #<=
16 | o.items.keys #<=
17 | o.items["RF-0034"].descr #<=
18 | o.items["RF-0034"].total_price #<=
19 | o.signatures #<=
20 | o.signatures[2].name #<=
21 | o.signatures[2].position #<=
22 | ## default value was set
23 |
24 | o.total_price #<=
25 |
26 | #:invisible:
27 | assert_equal "12343-AHSHE-314159", o.reference
28 | assert_equal "Jean Smith", o.client.name
29 | assert_equal "San Francisco", o.client.work_address.city
30 | assert_equal "San Mateo", o.client.home_address.city
31 | assert_equal %w{RF-0001 RF-0034 RF-3341}, o.items.keys.sort
32 | assert_equal ['John Doe','Jill Smith','Miles O\'Brien'], o.signatures.map{|s|s.name}
33 | assert_equal 2575, (10 * o.total_price).round
34 | #<=
35 | #:visible:
36 |
37 | ####write access
38 | o.client.name="James T. Kirk"
39 | o.items['RF-4711'] = Item.new
40 | o.items['RF-4711'].descr = 'power transfer grid'
41 | o.items['RF-4711'].quantity = 2
42 | o.items['RF-4711'].unit_price = 29.95
43 |
44 | s=Signature.new
45 | s.name='Harry Smith'
46 | s.position='general manager'
47 | o.signatures << s
48 | xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name) #<=
49 | #:invisible_retval:
50 | xml.write($stdout,2) #<=
51 |
52 | #:invisible:
53 | assert_equal %w{RF-0001 RF-0034 RF-3341 RF-4711}, xml.all_xpath("Item/@reference").map{|x|x.text}.sort
54 | assert_equal ['John Doe','Jill Smith','Miles O\'Brien','Harry Smith'],
55 | xml.all_xpath("Signed-By/Signature/Name").map{|x|x.text}
56 | #<=
57 | #:visible:
58 |
59 |
60 | #<=
61 | #:visible_retval:
62 | ####Starting a new order from scratch
63 | o = Order.new #<=
64 | ## attributes with default values (here: signatures) are set
65 | ## automatically
66 |
67 | #:handle_exceptions:
68 | xml=o.save_to_xml #<=
69 | #:no_exceptions:
70 | ## can't save as long as there are still unset attributes without
71 | ## default values
72 |
73 | o.reference = "FOOBAR-1234"
74 |
75 | o.client = Client.new
76 | o.client.name = 'Ford Prefect'
77 | o.client.home_address = Address.new
78 | o.client.home_address.street = '42 Park Av.'
79 | o.client.home_address.city = 'small planet'
80 | o.client.home_address.zip = 17263
81 | o.client.home_address.state = 'Betelgeuse system'
82 |
83 | o.items={'XY-42' => Item.new}
84 | o.items['XY-42'].descr = 'improbability drive'
85 | o.items['XY-42'].quantity = 3
86 | o.items['XY-42'].unit_price = 299.95
87 |
88 | #:invisible_retval:
89 | xml=o.save_to_xml
90 | xml.write($stdout,2)
91 | #<=
92 | #:invisible:
93 | assert_equal "order", xml.name
94 | assert_equal o.reference, xml.first_xpath("@reference").text
95 | assert_equal o.client.name, xml.first_xpath("Client/Name").text
96 | assert_equal o.client.home_address.street, xml.first_xpath("Client/Address[@where='home']/Street").text
97 | assert_equal o.client.home_address.city, xml.first_xpath("Client/Address[@where='home']/City").text
98 | assert_nil xml.first_xpath("Client/Address[@where='work']", :allow_nil=>true)
99 | assert_equal 1, xml.all_xpath("Client/Address").size
100 |
101 | o.client.work_address = Address.new
102 | o.client.work_address.street = 'milky way 2'
103 | o.client.work_address.city = 'Ursa Major'
104 | o.client.work_address.zip = 18293
105 | o.client.work_address.state = 'Magellan Cloud'
106 | xml=o.save_to_xml
107 |
108 | assert_equal o.client.work_address.street, xml.first_xpath("Client/Address[@where='work']/Street").text
109 | assert_equal o.client.work_address.city, xml.first_xpath("Client/Address[@where='work']/City").text
110 | assert_equal o.client.home_address.street, xml.first_xpath("Client/Address[@where='home']/Street").text
111 | assert_equal 2, xml.all_xpath("Client/Address").size
112 | #<=
113 | #:visible:
114 |
115 | ## the root element name when saving an object to XML will by default
116 | ## be derived from the class name (in this example, "Order" became
117 | ## "order"). This can be overridden on a per-class basis; see
118 | ## XML::Mapping::ClassMethods#root_element_name for details.
119 |
--------------------------------------------------------------------------------
/examples/person.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'xml/mapping'
4 | require 'xml/xxpath_methods'
5 | #<=
6 | #:visible:
7 |
8 | class Person
9 | include XML::Mapping
10 |
11 | choice_node :if, 'name', :then, (text_node :name, 'name'),
12 | :elsif, '@name', :then, (text_node :name, '@name'),
13 | :else, (text_node :name, '.')
14 | end
15 |
16 | ### usage
17 |
18 | p1 = Person.load_from_xml(REXML::Document.new('').root)#<=
19 |
20 | p2 = Person.load_from_xml(REXML::Document.new('James').root)#<=
21 |
22 | p3 = Person.load_from_xml(REXML::Document.new('Suzy').root)#<=
23 |
24 |
25 | #:invisible_retval:
26 | p1.save_to_xml.write($stdout)#<=
27 |
28 | p2.save_to_xml.write($stdout)#<=
29 |
30 | p3.save_to_xml.write($stdout)#<=
31 |
32 | #:invisible:
33 | require 'test/unit/assertions'
34 | include Test::Unit::Assertions
35 |
36 | assert_equal "Jim", p1.name
37 | assert_equal "James", p2.name
38 | assert_equal "Suzy", p3.name
39 |
40 | xml = p3.save_to_xml
41 | assert_equal "name", xml.elements[1].name
42 | assert_equal "Suzy", xml.elements[1].text
43 |
44 | #<=
45 |
--------------------------------------------------------------------------------
/examples/person_mm.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | begin
4 | Object.send(:remove_const, "Address") # remove any previous definitions
5 | Object.send(:remove_const, "Person") # remove any previous definitions
6 | rescue
7 | end
8 | #<=
9 | #:visible:
10 | require 'xml/mapping'
11 |
12 | class Address; end
13 |
14 | class Person
15 | include XML::Mapping
16 |
17 | # the default mapping. Stores the name and age in XML attributes,
18 | # and the address in a sub-element "address".
19 |
20 | text_node :name, "@name"
21 | numeric_node :age, "@age"
22 | object_node :address, "address", :class=>Address
23 |
24 | use_mapping :other
25 |
26 | # the ":other" mapping. Non-default root element name; name and age
27 | # stored in XML elements; address stored in the person's element
28 | # itself
29 |
30 | root_element_name "individual"
31 | text_node :name, "name"
32 | numeric_node :age, "age"
33 | object_node :address, ".", :class=>Address
34 |
35 | # you could also specify the mapping on a per-node basis with the
36 | # :mapping option, e.g.:
37 | #
38 | # numeric_node :age, "age", :mapping=>:other
39 | end
40 |
41 |
42 | class Address
43 | include XML::Mapping
44 |
45 | # the default mapping.
46 |
47 | text_node :street, "street"
48 | numeric_node :number, "number"
49 | text_node :city, "city"
50 | numeric_node :zip, "zip"
51 |
52 | use_mapping :other
53 |
54 | # the ":other" mapping.
55 |
56 | text_node :street, "street-name"
57 | numeric_node :number, "street-name/@number"
58 | text_node :city, "city-name"
59 | numeric_node :zip, "city-name/@zip-code"
60 | end
61 |
62 |
63 | ### usage
64 |
65 | ## XML representation of a person in the default mapping
66 | xml = REXML::Document.new('
67 |
68 |
69 | Abbey Road
70 | 72
71 | London
72 | 18827
73 |
74 | ').root
75 |
76 | ## load using the default mapping
77 | p = Person.load_from_xml xml #<=
78 |
79 | #:invisible_retval:
80 | ## save using the default mapping
81 | xml2 = p.save_to_xml
82 | xml2.write $stdout,2 #<=
83 |
84 | ## xml2 identical to xml
85 |
86 |
87 | ## now, save the same person to XML using the :other mapping...
88 | other_xml = p.save_to_xml :mapping=>:other
89 | other_xml.write $stdout,2 #<=
90 |
91 | #:visible_retval:
92 | ## load it again using the :other mapping
93 | p2 = Person.load_from_xml other_xml, :mapping=>:other #<=
94 |
95 | #:invisible_retval:
96 | ## p2 identical to p #<=
97 |
98 | #:invisible:
99 | require 'test/unit/assertions'
100 | include Test::Unit::Assertions
101 |
102 | require 'xml/xxpath_methods'
103 |
104 | assert_equal "Suzy", p.name
105 | assert_equal 28, p.age
106 | assert_equal "Abbey Road", p.address.street
107 | assert_equal 72, p.address.number
108 | assert_equal "London", p.address.city
109 | assert_equal 18827, p.address.zip
110 |
111 | assert_equal "individual", other_xml.name
112 | assert_equal p.name, other_xml.first_xpath("name").text
113 | assert_equal p.age, other_xml.first_xpath("age").text.to_i
114 | assert_equal p.address.street, other_xml.first_xpath("street-name").text
115 | assert_equal p.address.number, other_xml.first_xpath("street-name/@number").text.to_i
116 | assert_equal p.address.city, other_xml.first_xpath("city-name").text
117 | assert_equal p.address.zip, other_xml.first_xpath("city-name/@zip-code").text.to_i
118 |
119 | #<=
120 |
--------------------------------------------------------------------------------
/examples/publication.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'xml/mapping'
4 | require 'xml/xxpath_methods'
5 | #<=
6 | #:visible:
7 |
8 | class Publication
9 | include XML::Mapping
10 |
11 | choice_node :if, '@author', :then, (text_node :author, '@author'),
12 | :elsif, 'contr', :then, (array_node :contributors, 'contr', :class=>String)
13 | end
14 |
15 | ### usage
16 |
17 | p1 = Publication.load_from_xml(REXML::Document.new('').root)#<=
18 |
19 | p2 = Publication.load_from_xml(REXML::Document.new('
20 |
21 | Chris
22 | Mel
23 | Toby
24 | ').root)#<=
25 |
26 | #:invisible:
27 | require 'test/unit/assertions'
28 | include Test::Unit::Assertions
29 |
30 | assert_equal "Jim", p1.author
31 | assert_nil p1.contributors
32 |
33 | assert_nil p2.author
34 | assert_equal ["Chris", "Mel", "Toby"], p2.contributors
35 |
36 | xml1 = p1.save_to_xml
37 | xml2 = p2.save_to_xml
38 |
39 | assert_equal p1.author, xml1.first_xpath("@author").text
40 | assert_nil xml1.first_xpath("contr", :allow_nil=>true)
41 |
42 | assert_nil xml2.first_xpath("@author", :allow_nil=>true)
43 | assert_equal p2.contributors, xml2.all_xpath("contr").map{|elt|elt.text}
44 | #<=
45 |
--------------------------------------------------------------------------------
/examples/reader.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'xml/mapping'
4 | require 'xml/xxpath_methods'
5 | #<=
6 | #:visible:
7 |
8 | class Foo
9 | include XML::Mapping
10 |
11 | text_node :name, "@name", :reader=>proc{|obj,xml,default_reader|
12 | default_reader.call(obj,xml)
13 | obj.name += xml.attributes['more']
14 | },
15 | :writer=>proc{|obj,xml|
16 | xml.attributes['bar'] = "hi #{obj.name} ho"
17 | }
18 | end
19 |
20 | f = Foo.load_from_xml(REXML::Document.new('').root)#<=
21 |
22 | #:invisible_retval:
23 | xml = f.save_to_xml
24 | xml.write $stdout,2 #<=
25 |
26 | #:invisible:
27 | require 'test/unit/assertions'
28 | include Test::Unit::Assertions
29 |
30 | assert_equal "JimXYZ", f.name
31 | assert_equal "hi JimXYZ ho", xml.attributes['bar']
32 |
33 | #<=
34 |
--------------------------------------------------------------------------------
/examples/stringarray.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 | class People
3 | include XML::Mapping
4 | array_node :names, "names", "name", :class=>String
5 | end
6 |
--------------------------------------------------------------------------------
/examples/stringarray.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Jim
6 | Susan
7 | Herbie
8 | Nancy
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/stringarray_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'stringarray' #<=
4 | #:visible:
5 | ppl=People.load_from_file("stringarray.xml") #<=
6 | ppl.names #<=
7 |
8 | ppl.names.concat ["Mary","Arnold"] #<=
9 | #:invisible_retval:
10 | ppl.save_to_xml.write $stdout,2
11 | #<=
12 |
--------------------------------------------------------------------------------
/examples/time_augm.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'order' #<=
4 | #:visible:
5 | class Time
6 | include XML::Mapping
7 |
8 | numeric_node :year, "year"
9 | numeric_node :month, "month"
10 | numeric_node :day, "mday"
11 | numeric_node :hour, "hours"
12 | numeric_node :min, "minutes"
13 | numeric_node :sec, "seconds"
14 | end
15 |
16 |
17 | nowxml=Time.now.save_to_xml #<=
18 | #:invisible_retval:
19 | nowxml.write($stdout,2)#<=
20 |
--------------------------------------------------------------------------------
/examples/time_augm_loading.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib"
3 | require 'xml/mapping'
4 | require 'xml/xxpath_methods'
5 |
6 | class Time
7 | include XML::Mapping
8 |
9 | numeric_node :year, "year"
10 | numeric_node :month, "month"
11 | numeric_node :day, "mday"
12 | numeric_node :hour, "hours"
13 | numeric_node :min, "minutes"
14 | numeric_node :sec, "seconds"
15 | end
16 |
17 | #<=
18 | #:invisible_retval:
19 | #:visible:
20 |
21 | def Time.load_from_xml(xml, options={:mapping=>:_default})
22 | year,month,day,hour,min,sec =
23 | [xml.first_xpath("year").text.to_i,
24 | xml.first_xpath("month").text.to_i,
25 | xml.first_xpath("mday").text.to_i,
26 | xml.first_xpath("hours").text.to_i,
27 | xml.first_xpath("minutes").text.to_i,
28 | xml.first_xpath("seconds").text.to_i]
29 | Time.local(year,month,day,hour,min,sec)
30 | end
31 | #<=
32 | #:invisible:
33 | require 'test/unit/assertions'
34 | include Test::Unit::Assertions
35 |
36 | t = Time.now
37 | t2 = Time.load_from_xml(t.save_to_xml)
38 |
39 | assert_equal t.year, t2.year
40 | assert_equal t.month, t2.month
41 | assert_equal t.day, t2.day
42 | assert_equal t.hour, t2.hour
43 | assert_equal t.min, t2.min
44 | assert_equal t.sec, t2.sec
45 |
--------------------------------------------------------------------------------
/examples/time_node.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping/base'
2 |
3 | class TimeNode < XML::Mapping::SingleAttributeNode
4 | def initialize(*args)
5 | path,*args = super(*args)
6 | @y_path = XML::XXPath.new(path+"/year")
7 | @m_path = XML::XXPath.new(path+"/month")
8 | @d_path = XML::XXPath.new(path+"/day")
9 | args
10 | end
11 |
12 | def extract_attr_value(xml)
13 | y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i,
14 | @m_path.first(xml).text.to_i,
15 | @d_path.first(xml).text.to_i]
16 | }
17 | Time.local(y,m,d)
18 | end
19 |
20 | def set_attr_value(xml, value)
21 | @y_path.first(xml,:ensure_created=>true).text = value.year
22 | @m_path.first(xml,:ensure_created=>true).text = value.month
23 | @d_path.first(xml,:ensure_created=>true).text = value.day
24 | end
25 | end
26 |
27 |
28 | XML::Mapping.add_node_class TimeNode
29 |
--------------------------------------------------------------------------------
/examples/time_node_w_marshallers.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | #:invisible_retval:
3 | $:.unshift "../lib" #<=
4 | #:visible:
5 | require 'xml/mapping'
6 | require 'xml/xxpath_methods'
7 |
8 | class Signature
9 | include XML::Mapping
10 |
11 | text_node :name, "Name"
12 | text_node :position, "Position", :default_value=>"Some Employee"
13 | object_node :signed_on, "signed-on",
14 | :unmarshaller=>proc{|xml|
15 | y,m,d = [xml.first_xpath("year").text.to_i,
16 | xml.first_xpath("month").text.to_i,
17 | xml.first_xpath("day").text.to_i]
18 | Time.local(y,m,d)
19 | },
20 | :marshaller=>proc{|xml,value|
21 | e = xml.elements.add; e.name = "year"; e.text = value.year
22 | e = xml.elements.add; e.name = "month"; e.text = value.month
23 | e = xml.elements.add; e.name = "day"; e.text = value.day
24 |
25 | # xml.first("year",:ensure_created=>true).text = value.year
26 | # xml.first("month",:ensure_created=>true).text = value.month
27 | # xml.first("day",:ensure_created=>true).text = value.day
28 | }
29 | end #<=
30 | #:invisible:
31 | require 'test/unit/assertions'
32 |
33 | include Test::Unit::Assertions
34 |
35 | t=Time.local(2005,12,1)
36 |
37 | s=Signature.new
38 | s.name = "Olaf Klischat"; s.position="chief"; s.signed_on=t
39 | xml = s.save_to_xml
40 |
41 | assert_equal "2005", xml.first_xpath("signed-on/year").text
42 | assert_equal "12", xml.first_xpath("signed-on/month").text
43 | assert_equal "1", xml.first_xpath("signed-on/day").text
44 |
45 | s2 = Signature.load_from_xml xml
46 | assert_equal "Olaf Klischat", s2.name
47 | assert_equal "chief", s2.position
48 | assert_equal t, s2.signed_on
49 |
--------------------------------------------------------------------------------
/examples/time_node_w_marshallers.xml:
--------------------------------------------------------------------------------
1 |
2 | John Doe
3 | product manager
4 |
5 | 13
6 | 2
7 | 2005
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/xpath_create_new.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib" #<=
3 | #:visible:
4 | require 'xml/xxpath'
5 |
6 | d=REXML::Document.new <
8 |
9 | Java
10 | Ruby
11 |
12 |
13 | EOS
14 |
15 |
16 | rootelt=d.root
17 |
18 | #:invisible_retval:
19 | path1=XML::XXPath.new("/bar/baz[@key='work']")
20 |
21 | #:visible_retval:
22 | path1.create_new(rootelt)#<=
23 | #:invisible_retval:
24 | d.write($stdout,2)#<=
25 | ### a new element is created for *each* path element, regardless of
26 | ### what existed before. So a new "bar" element was added, with a new
27 | ### "baz" element inside it
28 |
29 | ### same call again...
30 | #:visible_retval:
31 | path1.create_new(rootelt)#<=
32 | #:invisible_retval:
33 | d.write($stdout,2)#<=
34 | ### same procedure -- new elements added for each path element
35 |
36 |
37 | #:visible_retval:
38 | ## get reference to 1st "baz" element
39 | firstbazelt=XML::XXPath.new("/bar/baz").first(rootelt)#<=
40 |
41 | #:invisible_retval:
42 | path2=XML::XXPath.new("@key2")
43 |
44 | #:visible_retval:
45 | path2.create_new(firstbazelt)#<=
46 | #:invisible_retval:
47 | d.write($stdout,2)#<=
48 | ### ok, new attribute node added
49 |
50 | ### same call again...
51 | #:visible_retval:
52 | #:handle_exceptions:
53 | path2.create_new(firstbazelt)#<=
54 | #:no_exceptions:
55 | ### can't create that path anew again -- an element can't have more
56 | ### than one attribute with the same name
57 |
58 | #:invisible_retval:
59 | ### the document hasn't changed
60 | d.write($stdout,2)#<=
61 |
62 |
63 |
64 | ### create_new the same path as in the ensure_created example
65 | #:visible_retval:
66 | baz6elt=XML::XXPath.new("/bar/baz[6]").create_new(rootelt)#<=
67 | #:invisible_retval:
68 | d.write($stdout,2)#<=
69 | ### ok, new "bar" element and 6th "baz" element inside it created
70 |
71 |
72 | #:visible_retval:
73 | #:handle_exceptions:
74 | XML::XXPath.new("baz[6]").create_new(baz6elt.parent)#<=
75 | #:no_exceptions:
76 | #:invisible_retval:
77 | ### yep, baz[6] already existed and thus couldn't be created once
78 | ### again
79 |
80 | ### but of course...
81 | #:visible_retval:
82 | XML::XXPath.new("/bar/baz[6]").create_new(rootelt)#<=
83 | #:invisible_retval:
84 | d.write($stdout,2)#<=
85 | ### this works because *all* path elements are newly created
86 |
--------------------------------------------------------------------------------
/examples/xpath_docvsroot.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib" #<=
3 | #:visible:
4 | require 'xml/xxpath'
5 |
6 | d=REXML::Document.new <
8 |
9 |
10 | pingpong
11 |
12 |
13 |
14 |
15 | EOS
16 |
17 | XML::XXPath.new("/foo/bar").all(d)#<=
18 |
19 | XML::XXPath.new("/bar").all(d)#<=
20 |
21 | XML::XXPath.new("/foo/bar").all(d.root)#<=
22 |
23 | XML::XXPath.new("/bar").all(d.root)#<=
24 |
25 |
26 | firstelt = XML::XXPath.new("/foo/bar/first").first(d)#<=
27 |
28 | XML::XXPath.new("/first/second").all(firstelt)#<=
29 |
30 | XML::XXPath.new("/second").all(firstelt)#<=
31 |
--------------------------------------------------------------------------------
/examples/xpath_ensure_created.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib" #<=
3 | #:visible:
4 | require 'xml/xxpath'
5 |
6 | d=REXML::Document.new <
8 |
9 | Java
10 | Ruby
11 |
12 |
13 | EOS
14 |
15 |
16 | rootelt=d.root
17 |
18 | #### ensuring that a specific path exists inside the document
19 |
20 | #:visible_retval:
21 | XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)#<=
22 | #:invisible_retval:
23 | d.write($stdout,2)#<=
24 | ### no change (path existed before)
25 |
26 |
27 | #:visible_retval:
28 | XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)#<=
29 | #:invisible_retval:
30 | d.write($stdout,2)#<=
31 | ### path was added
32 |
33 | #:visible_retval:
34 | XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)#<=
35 | #:invisible_retval:
36 | d.write($stdout,2)#<=
37 | ### no change this time
38 |
39 | #:visible_retval:
40 | XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)#<=
41 | #:invisible_retval:
42 | d.write($stdout,2)#<=
43 | ### this fit in the 1st "baz" element since
44 | ### there was no "key2" attribute there before.
45 |
46 | #:visible_retval:
47 | XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)#<=
48 | #:invisible_retval:
49 | d.write($stdout,2)#<=
50 | ### no change
51 |
52 | #:visible_retval:
53 | XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)#<=
54 | #:invisible_retval:
55 | d.write($stdout,2)#<=
56 | ### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
57 |
58 | #:visible_retval:
59 | XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)#<=
60 | #:invisible_retval:
61 | d.write($stdout,2)#<=
62 | ### no change this time
63 |
--------------------------------------------------------------------------------
/examples/xpath_pathological.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib" #<=
3 | #:visible:
4 | require 'xml/xxpath'
5 |
6 | d=REXML::Document.new <
8 |
9 |
10 |
11 | EOS
12 |
13 |
14 | rootelt=d.root
15 |
16 |
17 | XML::XXPath.new("*").all(rootelt)#<=
18 | ### ok
19 |
20 | XML::XXPath.new("bar/*").first(rootelt, :allow_nil=>true)#<=
21 | ### ok, nothing there
22 |
23 | ### the same call with :ensure_created=>true
24 | newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)#<=
25 |
26 | #:invisible_retval:
27 | d.write($stdout,2)#<=
28 |
29 | #:visible_retval:
30 | ### a new "unspecified" element was created
31 | newelt.unspecified?#<=
32 |
33 | ### we must modify it to "specify" it
34 | newelt.name="new-one"
35 | newelt.text="hello!"
36 | newelt.unspecified?#<=
37 |
38 | #:invisible_retval:
39 | d.write($stdout,2)#<=
40 |
41 | ### you could also set unspecified to false explicitly, as in:
42 | newelt.unspecified=true
43 |
--------------------------------------------------------------------------------
/examples/xpath_usage.intin.rb:
--------------------------------------------------------------------------------
1 | #:invisible:
2 | $:.unshift "../lib" #<=
3 | #:visible:
4 | require 'xml/xxpath'
5 |
6 | d=REXML::Document.new <
8 |
9 | Java
10 | Ruby
11 |
12 |
13 | hello
14 | scrabble
15 | goodbye
16 |
17 |
18 | poker
19 |
20 |
21 | EOS
22 |
23 |
24 | ####read access
25 | path=XML::XXPath.new("/foo/bar[2]/baz")
26 |
27 | ## path.all(document) gives all elements matching path in document
28 | path.all(d)#<=
29 |
30 | ## loop over them
31 | path.each(d){|elt| puts elt.text}#<=
32 |
33 | ## the first of those
34 | path.first(d)#<=
35 |
36 | ## no match here (only three "baz" elements)
37 | path2=XML::XXPath.new("/foo/bar[2]/baz[4]")
38 | path2.all(d)#<=
39 |
40 | #:handle_exceptions:
41 | ## "first" raises XML::XXPathError in such cases...
42 | path2.first(d)#<=
43 | #:no_exceptions:
44 |
45 | ##...unless we allow nil returns
46 | path2.first(d,:allow_nil=>true)#<=
47 |
48 | ##attribute nodes can also be returned
49 | keysPath=XML::XXPath.new("/foo/*/*/@key")
50 |
51 | keysPath.all(d).map{|attr|attr.text}#<=
52 |
--------------------------------------------------------------------------------
/lib/xml/mapping.rb:
--------------------------------------------------------------------------------
1 | # xml-mapping -- bidirectional Ruby-XML mapper
2 | # Copyright (C) 2004-2006 Olaf Klischat
3 |
4 | require 'xml/mapping/base'
5 | require 'xml/mapping/standard_nodes'
6 |
7 | XML::Mapping.add_node_class XML::Mapping::Node
8 | XML::Mapping.add_node_class XML::Mapping::TextNode
9 | XML::Mapping.add_node_class XML::Mapping::NumericNode
10 | XML::Mapping.add_node_class XML::Mapping::ObjectNode
11 | XML::Mapping.add_node_class XML::Mapping::BooleanNode
12 | XML::Mapping.add_node_class XML::Mapping::ArrayNode
13 | XML::Mapping.add_node_class XML::Mapping::HashNode
14 | XML::Mapping.add_node_class XML::Mapping::ChoiceNode
15 |
--------------------------------------------------------------------------------
/lib/xml/mapping/base.rb:
--------------------------------------------------------------------------------
1 | # xml-mapping -- bidirectional Ruby-XML mapper
2 | # Copyright (C) 2004-2006 Olaf Klischat
3 |
4 | require 'rexml/document'
5 | require "xml/xxpath"
6 |
7 | module XML
8 |
9 | class MappingError < RuntimeError
10 | end
11 |
12 | # This is the central interface module of the xml-mapping library.
13 | #
14 | # Including this module in your classes adds XML mapping
15 | # capabilities to them.
16 | #
17 | # == Example
18 | #
19 | # === Input document:
20 | #
21 | # :include: company.xml
22 | #
23 | # === mapping class declaration:
24 | #
25 | # :include: company.rb
26 | #
27 | # === usage:
28 | #
29 | # :include: company_usage.intout
30 | #
31 | # So you have to include XML::Mapping into your class to turn it
32 | # into a "mapping class", that is, to add XML mapping capabilities
33 | # to it. An instance of the mapping classes is then bidirectionally
34 | # mapped to an XML node (i.e. an element), where the state (simple
35 | # attributes, sub-objects, arrays, hashes etc.) of that instance is
36 | # mapped to sub-nodes of that node. In addition to the class and
37 | # instance methods defined in XML::Mapping, your mapping class will
38 | # get class methods like 'text_node', 'array_node' and so on; I call
39 | # them "node factory methods". More precisely, there is one node
40 | # factory method for each registered node type. Node types
41 | # are classes derived from XML::Mapping::Node; they're registered
42 | # with the xml-mapping library via XML::Mapping.add_node_class. The
43 | # node types TextNode, BooleanNode, NumericNode, ObjectNode,
44 | # ArrayNode, and HashNode are automatically registered by
45 | # xml/mapping.rb; you can easily write your own ones. The name of a
46 | # node factory method is inferred by 'underscoring' the name of the
47 | # corresponding node type; e.g. 'TextNode' becomes 'text_node'. Each
48 | # node factory method creates an instance of the corresponding node
49 | # type and adds it to the mapping class (not its instances). The
50 | # arguments to a node factory method are automatically turned into
51 | # arguments to the corresponding node type's initializer. So, in
52 | # order to learn more about the meaning of a node factory method's
53 | # parameters, you read the documentation of the corresponding node
54 | # type. All predefined node types expect as their first argument a
55 | # symbol that names an r/w attribute which will be added to the
56 | # mapping class. The mapping class is a normal Ruby class; you can
57 | # add constructors, methods and attributes to it, derive from it,
58 | # derive it from another class, include additional modules etc.
59 | #
60 | # Including XML::Mapping also adds all methods of
61 | # XML::Mapping::ClassMethods to your class (as class methods).
62 | #
63 | # It is recommended that if your class does not have required
64 | # +initialize+ method arguments. The XML loader attempts to create a
65 | # new object using the +new+ method. If this fails because the
66 | # initializer expects an argument, then the loader calls +allocate+
67 | # instead. +allocate+ bypasses the initializer. If your class must
68 | # have initializer arguments, then you should verify that bypassing
69 | # the initializer is acceptable.
70 | #
71 | # As you may have noticed from the example, the node factory methods
72 | # generally use XPath expressions to specify locations in the mapped
73 | # XML document. To make this work, XML::Mapping relies on
74 | # XML::XXPath, which implements a subset of XPath, but also provides
75 | # write access, which is needed by the node types to support writing
76 | # data back to XML. Both XML::Mapping and XML::XXPath use REXML
77 | # (http://www.germane-software.com/software/rexml/) to represent XML
78 | # elements/documents in memory.
79 | module Mapping
80 |
81 | # defined mapping classes for a given root elt name and mapping
82 | # name (nested map from root element name to mapping name to array
83 | # of classes)
84 | #
85 | # can't really use a class variable for this because it must be
86 | # shared by all class methods mixed into classes by including
87 | # Mapping. See
88 | # http://multi-io.github.io/mydocs-pub/ruby/mixin_class_methods_global_state.txt.html
89 | # for a more detailed discussion.
90 | Classes_by_rootelt_names = {} #:nodoc:
91 | class << Classes_by_rootelt_names
92 | def create_classes_for rootelt_name, mapping
93 | (self[rootelt_name] ||= {})[mapping] ||= []
94 | end
95 | def classes_for rootelt_name, mapping
96 | (self[rootelt_name] || {})[mapping] || []
97 | end
98 | def remove_class rootelt_name, mapping, clazz
99 | classes_for(rootelt_name, mapping).delete clazz
100 | end
101 | def ensure_exists rootelt_name, mapping, clazz
102 | clazzes = create_classes_for(rootelt_name, mapping)
103 | clazzes << clazz unless clazzes.include? clazz
104 | end
105 | end
106 |
107 |
108 | def self.append_features(base) #:nodoc:
109 | super
110 | base.extend(ClassMethods)
111 | Classes_by_rootelt_names.create_classes_for(base.default_root_element_name, :_default) << base
112 | base.initializing_xml_mapping
113 | end
114 |
115 | # Finds a mapping class corresponding to the given XML root
116 | # element name and mapping name. There may be more than one such class --
117 | # in that case, the most recently defined one is returned
118 | #
119 | # This is the inverse operation to
120 | # .root_element_name (see
121 | # XML::Mapping::ClassMethods.root_element_name).
122 | def self.class_for_root_elt_name(name, options={:mapping=>:_default})
123 | # TODO: implement Hash read-only instead of this
124 | # interface
125 | Classes_by_rootelt_names.classes_for(name, options[:mapping])[-1]
126 | end
127 |
128 | # Finds a mapping class and mapping name corresponding to the
129 | # given XML root element name. There may be more than one
130 | # (class,mapping) tuple for a given root element name -- in that
131 | # case, one of them is selected arbitrarily.
132 | #
133 | # returns [class,mapping]
134 | def self.class_and_mapping_for_root_elt_name(name)
135 | (Classes_by_rootelt_names[name] || {}).each_pair{|mapping,classes| return [classes[0],mapping] }
136 | nil
137 | end
138 |
139 | # Xml-mapping-specific initializer.
140 | #
141 | # This will be called when a new instance is being initialized
142 | # from an XML source, as well as after calling _class_._new_(args)
143 | # (for the latter case to work, you'll have to make sure you call
144 | # the inherited _initialize_ method)
145 | #
146 | # The :mapping keyword argument gives the mapping the instance is
147 | # being initialized with. This is non-nil only when the instance
148 | # is being initialized from an XML source (:mapping will contain
149 | # the :mapping argument passed (explicitly or implicitly) to the
150 | # load_from_... method).
151 | #
152 | # When the instance is being initialized because _class_._new_ was
153 | # called, the :mapping argument is set to nil to show that the
154 | # object is being initialized with respect to no specific mapping.
155 | #
156 | # The default implementation of this method calls obj_initializing
157 | # on all nodes. You may overwrite this method to do your own
158 | # initialization stuff; make sure to call +super+ in that case.
159 | def initialize_xml_mapping(options={:mapping=>nil})
160 | self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
161 | node.obj_initializing(self,options[:mapping])
162 | end
163 | end
164 |
165 | # Initializer. Called (by Class#new) after _self_ was created
166 | # using _new_.
167 | #
168 | # XML::Mapping's implementation calls #initialize_xml_mapping.
169 | def initialize(*args)
170 | super(*args)
171 | initialize_xml_mapping
172 | end
173 |
174 | # "fill" the contents of _xml_ into _self_. _xml_ is a
175 | # REXML::Element.
176 | #
177 | # First, pre_load(_xml_) is called, then all the nodes for this
178 | # object's class are processed (i.e. have their
179 | # #xml_to_obj method called) in the order of their definition
180 | # inside the class, then #post_load is called.
181 | def fill_from_xml(xml, options={:mapping=>:_default})
182 | raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") \
183 | unless self.class.xml_mapping_nodes_hash.has_key?(options[:mapping])
184 | pre_load xml, :mapping=>options[:mapping]
185 | self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
186 | node.xml_to_obj self, xml
187 | end
188 | post_load :mapping=>options[:mapping]
189 | end
190 |
191 | # This method is called immediately before _self_ is filled from
192 | # an xml source. _xml_ is the source REXML::Element.
193 | #
194 | # The default implementation of this method is empty.
195 | def pre_load(xml, options={:mapping=>:_default})
196 | end
197 |
198 |
199 | # This method is called immediately after _self_ has been filled
200 | # from an xml source. If you have things to do after the object
201 | # has been succefully loaded from the xml (reorganising the loaded
202 | # data in some way, setting up additional views on the data etc.),
203 | # this is the place where you put them. You can also raise an
204 | # exception to abandon the whole loading process.
205 | #
206 | # The default implementation of this method is empty.
207 | def post_load(options={:mapping=>:_default})
208 | end
209 |
210 |
211 | # Fill _self_'s state into the xml node (REXML::Element)
212 | # _xml_. All the nodes for this object's class are processed
213 | # (i.e. have their
214 | # #obj_to_xml method called) in the order of their definition
215 | # inside the class.
216 | def fill_into_xml(xml, options={:mapping=>:_default})
217 | self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
218 | node.obj_to_xml self,xml
219 | end
220 | end
221 |
222 | # Fill _self_'s state into a new xml node, return that
223 | # node.
224 | #
225 | # This method calls #pre_save, then #fill_into_xml, then
226 | # #post_save.
227 | def save_to_xml(options={:mapping=>:_default})
228 | xml = pre_save :mapping=>options[:mapping]
229 | fill_into_xml xml, :mapping=>options[:mapping]
230 | post_save xml, :mapping=>options[:mapping]
231 | xml
232 | end
233 |
234 | # This method is called when _self_ is to be converted to an XML
235 | # tree. It *must* create and return an XML element (as a
236 | # REXML::Element); that element will then be passed to
237 | # #fill_into_xml.
238 | #
239 | # The default implementation of this method creates a new empty
240 | # element whose name is the #root_element_name of _self_'s class
241 | # (see ClassMethods.root_element_name). By default, this is the
242 | # class name, with capital letters converted to lowercase and
243 | # preceded by a dash, e.g. "MySampleClass" becomes
244 | # "my-sample-class".
245 | def pre_save(options={:mapping=>:_default})
246 | REXML::Element.new(self.class.root_element_name(:mapping=>options[:mapping]))
247 | end
248 |
249 | # This method is called immediately after _self_'s state has been
250 | # filled into an XML element.
251 | #
252 | # The default implementation does nothing.
253 | def post_save(xml, options={:mapping=>:_default})
254 | end
255 |
256 |
257 | # Save _self_'s state as XML into the file named _filename_.
258 | # The XML is obtained by calling #save_to_xml.
259 | def save_to_file(filename, options={:mapping=>:_default})
260 | xml = save_to_xml :mapping=>options[:mapping]
261 | formatter = options[:formatter] || self.class.mapping_output_formatter
262 | File.open(filename,"w") do |f|
263 | formatter.write(xml, f)
264 | end
265 | end
266 |
267 |
268 | # The instance methods of this module are automatically added as
269 | # class methods to a class that includes XML::Mapping.
270 | module ClassMethods
271 | #ClassMethods = Module.new do # this is the alternative -- but see above for peculiarities
272 |
273 | # all nodes of this class, in the order of their definition,
274 | # hashed by mapping (hash mapping => array of nodes)
275 | def xml_mapping_nodes_hash #:nodoc:
276 | @xml_mapping_nodes ||= {}
277 | end
278 |
279 | # called on a class when it is being made a mapping class
280 | # (i.e. immediately after XML::Mapping was included in it)
281 | def initializing_xml_mapping #:nodoc:
282 | @default_mapping = :_default
283 | end
284 |
285 | # Make _mapping_ the mapping to be used by default in future
286 | # node declarations in this class. The default can be
287 | # overwritten on a per-node basis by passing a :mapping option
288 | # parameter to the node factory method
289 | #
290 | # The initial default mapping in a mapping class is :_default
291 | def use_mapping mapping
292 | @default_mapping = mapping
293 | xml_mapping_nodes_hash[mapping] ||= [] # create empty mapping node list if
294 | # there wasn't one before so future calls
295 | # to load/save_xml etc. w/ this mapping don't raise
296 | end
297 |
298 | # return the current default mapping (:_default initially, or
299 | # the value set with the latest call to use_mapping)
300 | def default_mapping
301 | @default_mapping
302 | end
303 |
304 | # Add getter and setter methods for a new attribute named _name_
305 | # (must be a symbol or a string) to this class, taking care not
306 | # to replace existing getters/setters. This is a convenience
307 | # method intended to be called from Node class initializers.
308 | def add_accessor(name)
309 | # existing methods search. Search for symbols and strings
310 | # to be compatible with Ruby 1.8 and 1.9.
311 | methods = self.instance_methods
312 | if methods[0].kind_of? Symbol
313 | getter = :"#{name}"
314 | setter = :"#{name}="
315 | else
316 | getter = "#{name}"
317 | setter = "#{name}="
318 | end
319 | unless methods.include?(getter)
320 | self.module_eval <<-EOS
321 | attr_reader :#{name}
322 | EOS
323 | end
324 | unless methods.include?(setter)
325 | self.module_eval <<-EOS
326 | attr_writer :#{name}
327 | EOS
328 | end
329 | end
330 |
331 | # Create a new instance of this class from the XML contained in
332 | # the file named _filename_. Calls load_from_xml internally.
333 | def load_from_file(filename, options={:mapping=>:_default})
334 | xml = REXML::Document.new(File.new(filename))
335 | load_from_xml xml.root, :mapping=>options[:mapping]
336 | end
337 |
338 | # Create a new instance of this class from the XML contained in
339 | # _xml_ (a REXML::Element).
340 | #
341 | # Allocates a new object, then calls fill_from_xml(_xml_) on
342 | # it.
343 | def load_from_xml(xml, options={:mapping=>:_default})
344 | raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") \
345 | unless xml_mapping_nodes_hash.has_key?(options[:mapping])
346 | # create the new object. It is recommended that the class
347 | # have a no-argument initializer, so try new first. If that
348 | # doesn't work, try allocate, which bypasses the initializer.
349 | begin
350 | obj = self.new
351 | #TODO: this will normally invoke our base XML::Mapping#initialize, which calls
352 | # obj.initialize_xml_mapping, which is called below again (with the correct :mapping parameter).
353 | # obj.initialize_xml_mapping calls obj_initializing on all nodes.
354 | # So obj_initializing may be called on the nodes twice for this initialization.
355 | # Maybe document this for node writers?
356 | rescue ArgumentError # TODO: this may hide real errors.
357 | # how to statically check whether
358 | # self self.new accepts an empty
359 | # argument list?
360 | obj = self.allocate
361 | end
362 | obj.initialize_xml_mapping :mapping=>options[:mapping]
363 | obj.fill_from_xml xml, :mapping=>options[:mapping]
364 | obj
365 | end
366 |
367 |
368 | # array of all nodes defined in this class, in the order of
369 | # their definition. Option :create specifies whether or not an
370 | # empty array should be created and returned if there was none
371 | # before (if not, an exception is raised). :mapping specifies
372 | # the mapping the returned nodes must have been defined in; nil
373 | # means return all nodes regardless of their mapping
374 | def xml_mapping_nodes(options={:mapping=>nil,:create=>true})
375 | unless options[:mapping]
376 | return xml_mapping_nodes_hash.values.inject([]){|a1,a2|a1+a2}
377 | end
378 | options[:create] = true if options[:create].nil?
379 | if options[:create]
380 | xml_mapping_nodes_hash[options[:mapping]] ||= []
381 | else
382 | xml_mapping_nodes_hash[options[:mapping]] ||
383 | raise(MappingError, "undefined mapping: #{options[:mapping].inspect}")
384 | end
385 | end
386 |
387 |
388 | # enumeration of all nodes in effect when
389 | # marshalling/unmarshalling this class, that is, nodes defined
390 | # for this class as well as for its superclasses. The nodes are
391 | # returned in the order of their definition, starting with the
392 | # topmost superclass that has nodes defined. keyword arguments
393 | # are the same as for #xml_mapping_nodes.
394 | def all_xml_mapping_nodes(options={:mapping=>nil,:create=>true})
395 | # TODO: we could return a dynamic Enumerable here, or cache
396 | # the array...
397 | result = []
398 | if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
399 | result += superclass.all_xml_mapping_nodes options
400 | end
401 | result += xml_mapping_nodes options
402 | end
403 |
404 |
405 | # The "root element name" of this class (combined getter/setter
406 | # method).
407 | #
408 | # The root element name is the name of the root element of the
409 | # XML tree returned by .#save_to_xml (or, more
410 | # specifically, .#pre_save). By default, this method
411 | # returns the #default_root_element_name; you may call this
412 | # method with an argument to set the root element name to
413 | # something other than the default. The option argument :mapping
414 | # specifies the mapping the root element is/will be defined in,
415 | # it defaults to the current default mapping (:_default
416 | # initially, or the value set with the latest call to
417 | # use_mapping)
418 | def root_element_name(name=nil, options={:mapping=>@default_mapping})
419 | if Hash===name # ugly...
420 | options=name; name=nil
421 | end
422 | @root_element_names ||= {}
423 | if name
424 | Classes_by_rootelt_names.remove_class root_element_name, options[:mapping], self
425 | @root_element_names[options[:mapping]] = name
426 | Classes_by_rootelt_names.create_classes_for(name, options[:mapping]) << self
427 | end
428 | @root_element_names[options[:mapping]] || default_root_element_name
429 | end
430 |
431 | # The default root element name for this class. Equals the class
432 | # name, with all parent module names stripped, and with capital
433 | # letters converted to lowercase and preceded by a dash;
434 | # e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
435 | def default_root_element_name
436 | self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
437 | end
438 |
439 | # the formatter to be used for output formatting when writing
440 | # xml to character streams (Files/IOs). Combined
441 | # getter/setter. Defaults to simple (compact/no-whitespace)
442 | # formatting, may be overridden on a per-call base via options
443 | def mapping_output_formatter(formatter=nil)
444 | # TODO make it per-mapping
445 | if formatter
446 | @mapping_output_formatter = formatter
447 | end
448 | @mapping_output_formatter ||= REXML::Formatters::Default.new
449 | end
450 | end
451 |
452 |
453 |
454 | # "polymorphic" load function. Turns the XML tree _xml_ into an
455 | # object, which is returned. The class of the object and the
456 | # mapping to be used for unmarshalling are automatically
457 | # determined from the root element name of _xml_ using
458 | # XML::Mapping.class_for_root_elt_name. If :mapping is non-nil,
459 | # only root element names defined in that mapping will be
460 | # considered (default is to consider all classes)
461 | def self.load_object_from_xml(xml,options={:mapping=>nil})
462 | if mapping = options[:mapping]
463 | c = class_for_root_elt_name xml.name, :mapping=>mapping
464 | else
465 | c,mapping = class_and_mapping_for_root_elt_name(xml.name)
466 | end
467 | unless c
468 | raise MappingError, "no mapping class for root element name #{xml.name}, mapping #{mapping.inspect}"
469 | end
470 | c.load_from_xml xml, :mapping=>mapping
471 | end
472 |
473 | # Like load_object_from_xml, but loads from the XML file named by
474 | # _filename_.
475 | def self.load_object_from_file(filename,options={:mapping=>nil})
476 | xml = REXML::Document.new(File.new(filename))
477 | load_object_from_xml xml.root, options
478 | end
479 |
480 |
481 | # Registers the new node class _c_ (must be a descendant of Node)
482 | # with the xml-mapping framework.
483 | #
484 | # A new "factory method" will automatically be added to
485 | # ClassMethods (and therefore to all classes that include
486 | # XML::Mapping from now on); so you can call it from the body of
487 | # your mapping class definition in order to create nodes of type
488 | # _c_. The name of the factory method is derived by "underscoring"
489 | # the (unqualified) name of _c_;
490 | # e.g. _c_==Foo::Bar::MyNiftyNode will result in the
491 | # creation of a factory method named +my_nifty_node+. The
492 | # generated factory method creates and returns a new instance of
493 | # _c_. The list of argument to _c_.new consists of _self_
494 | # (i.e. the mapping class the factory method was called from)
495 | # followed by the arguments passed to the factory method. You
496 | # should always use the factory methods to create instances of
497 | # node classes; you should never need to call a node class's
498 | # constructor directly.
499 | #
500 | # For a demonstration, see the calls to +text_node+, +array_node+
501 | # etc. in the examples along with the corresponding node classes
502 | # TextNode, ArrayNode etc. (these predefined node classes are in
503 | # no way "special"; they're added using add_node_class in
504 | # mapping.rb just like any custom node classes would be).
505 | def self.add_node_class(c)
506 | meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
507 | ClassMethods.module_eval <<-EOS
508 | def #{meth_name}(*args)
509 | #{c.name}.new(self,*args)
510 | end
511 | EOS
512 | end
513 |
514 |
515 | ###### core node classes
516 |
517 | # Abstract base class for all node types. As mentioned in the
518 | # documentation for XML::Mapping, node types must be registered
519 | # using XML::Mapping.add_node_class, and a corresponding "node
520 | # factory method" (e.g. "text_node") will then be added as a class
521 | # method to your mapping classes. The node factory method is
522 | # called from the body of the mapping classes as demonstrated in
523 | # the examples. It creates an instance of its corresponding node
524 | # type (the list of parameters to the node factory method,
525 | # preceded by the owning mapping class, will be passed to the
526 | # constructor of the node type) and adds it to its owning mapping
527 | # class, so there is one node object per node definition per
528 | # mapping class. That node object will handle all XML
529 | # marshalling/unmarshalling for this node, for all instances of
530 | # the mapping class. For this purpose, the marshalling and
531 | # unmarshalling methods of a mapping class instance (fill_into_xml
532 | # and fill_from_xml, respectively) will call obj_to_xml
533 | # resp. xml_to_obj on all nodes of the mapping class, in the order
534 | # of their definition, passing the REXML element the data is to be
535 | # marshalled to/unmarshalled from as well as the object the data
536 | # is to be read from/filled into.
537 | #
538 | # Node types that map some XML data to a single attribute of their
539 | # mapping class (that should be most of them) shouldn't be
540 | # directly derived from this class, but rather from
541 | # SingleAttributeNode.
542 | class Node
543 | # Intializer, to be called from descendant classes. _owner_ is
544 | # the mapping class this node is being defined in. It'll be
545 | # stored in _@owner_. @options will be set to a (possibly empty)
546 | # hash containing the option arguments passed to
547 | # _initialize_. Options :mapping, :reader and :writer will be
548 | # handled, subclasses may handle additional options. See the
549 | # section on defining nodes in the README for details.
550 | def initialize(owner,*args)
551 | @owner = owner
552 | if Hash===args[-1]
553 | @options = args[-1]
554 | args = args[0..-2]
555 | else
556 | @options={}
557 | end
558 | @mapping = @options[:mapping] || owner.default_mapping
559 | owner.xml_mapping_nodes(:mapping=>@mapping) << self
560 | XML::Mapping::Classes_by_rootelt_names.ensure_exists owner.root_element_name, @mapping, owner
561 | if @options[:reader]
562 | # override xml_to_obj in this instance with invocation of
563 | # @options[:reader]
564 | class << self
565 | alias_method :default_xml_to_obj, :xml_to_obj
566 | def xml_to_obj(obj,xml)
567 | begin
568 | @options[:reader].call(obj,xml,self.method(:default_xml_to_obj))
569 | rescue ArgumentError # thrown if @options[:reader] is a lambda (i.e. no Proc) with !=3 args (e.g. proc{...} in ruby1.8)
570 | @options[:reader].call(obj,xml)
571 | end
572 | end
573 | end
574 | end
575 | if @options[:writer]
576 | # override obj_to_xml in this instance with invocation of
577 | # @options[:writer]
578 | class << self
579 | alias_method :default_obj_to_xml, :obj_to_xml
580 | def obj_to_xml(obj,xml)
581 | begin
582 | @options[:writer].call(obj,xml,self.method(:default_obj_to_xml))
583 | rescue ArgumentError # thrown if (see above)
584 | @options[:writer].call(obj,xml)
585 | end
586 | end
587 | end
588 | end
589 | args
590 | end
591 | # This is called by the XML unmarshalling machinery when the
592 | # state of an instance of this node's @owner is to be read from
593 | # an XML tree. _obj_ is the instance, _xml_ is the tree (a
594 | # REXML::Element). The node must read "its" data from _xml_
595 | # (using XML::XXPath or any other means) and store it to the
596 | # corresponding parts (attributes etc.) of _obj_'s state.
597 | def xml_to_obj(obj,xml)
598 | raise "abstract method called"
599 | end
600 | # This is called by the XML unmarshalling machinery when the
601 | # state of an instance of this node's @owner is to be stored
602 | # into an XML tree. _obj_ is the instance, _xml_ is the tree (a
603 | # REXML::Element). The node must extract "its" data from _obj_
604 | # and store it to the corresponding parts (sub-elements,
605 | # attributes etc.) of _xml_ (using XML::XXPath or any other
606 | # means).
607 | def obj_to_xml(obj,xml)
608 | raise "abstract method called"
609 | end
610 | # Called when a new instance of the mapping class this node
611 | # belongs to is being initialized. _obj_ is the
612 | # instance. _mapping_ is the mapping the initialization is
613 | # happening with, if any: If the instance is being initialized
614 | # as part of e.g. Class.load_from_file(name,
615 | # :mapping=>:some_mapping or any other call that specifies
616 | # a mapping, that mapping will be passed to this method. If the
617 | # instance is being initialized normally with
618 | # Class.new, _mapping_ is nil here.
619 | #
620 | # You may set up initial values for the attributes this node is
621 | # responsible for here. Default implementation is empty.
622 | def obj_initializing(obj,mapping)
623 | end
624 | # tell whether this node's data is present in _obj_ (when this
625 | # method is called, _obj_ will be an instance of the mapping
626 | # class this node was defined in). This method is currently used
627 | # only by ChoiceNode when writing data back to XML. See
628 | # ChoiceNode#obj_to_xml.
629 | def is_present_in? obj
630 | true
631 | end
632 | end
633 |
634 |
635 | # Base class for node types that map some XML data to a single
636 | # attribute of their mapping class.
637 | #
638 | # All node types that come with xml-mapping except one
639 | # (ChoiceNode) inherit from SingleAttributeNode.
640 | class SingleAttributeNode < Node
641 | # Initializer. _owner_ is the owning mapping class (gets passed
642 | # to the superclass initializer and therefore put into
643 | # @owner). The second parameter (and hence the first parameter
644 | # to the node factory method), _attrname_, is a symbol that
645 | # names the mapping class attribute this node should map to. It
646 | # gets stored into @attrname, and the attribute (an r/w
647 | # attribute of name attrname) is added to the mapping class
648 | # (using attr_accessor).
649 | #
650 | # In the initializer, two option arguments -- :optional and
651 | # :default_value -- are processed in SingleAttributeNode:
652 | #
653 | # Supplying :default_value=>_obj_ makes _obj_ the _default
654 | # value_ for this attribute. When unmarshalling (loading) an
655 | # object from an XML source, the attribute will be set to this
656 | # value if nothing was provided in the XML; when marshalling
657 | # (saving), the attribute won't be saved if it is set to the
658 | # default value.
659 | #
660 | # Providing just :optional=>true is equivalent to providing
661 | # :default_value=>nil.
662 | def initialize(*args)
663 | @attrname,*args = super(*args)
664 | @owner.add_accessor @attrname
665 | if @options[:optional] and not(@options.has_key?(:default_value))
666 | @options[:default_value] = nil
667 | end
668 | initialize_impl(*args)
669 | args
670 | end
671 | # this method was retained for compatibility with xml-mapping 0.8.
672 | #
673 | # It used to be the initializer to be implemented by subclasses. The
674 | # arguments (args) are those still unprocessed by
675 | # SingleAttributeNode's initializer.
676 | #
677 | # In xml-mapping 0.9 and up, you should just override initialize() and
678 | # call super.initialize. The returned array is the same args array.
679 | def initialize_impl(*args)
680 | end
681 |
682 | # Exception that may be used by implementations of
683 | # #extract_attr_value to announce that the attribute value is
684 | # not set in the XML and, consequently, the default value should
685 | # be set in the object being created, or an Exception be raised
686 | # if no default value was specified.
687 | class NoAttrValueSet < XXPathError
688 | end
689 |
690 | def xml_to_obj(obj,xml) # :nodoc:
691 | begin
692 | obj.send :"#{@attrname}=", extract_attr_value(xml)
693 | rescue NoAttrValueSet => err
694 | unless @options.has_key? :default_value
695 | raise XML::MappingError, "no value, and no default value: #{err}"
696 | end
697 | begin
698 | obj.send :"#{@attrname}=", @options[:default_value].clone
699 | rescue
700 | obj.send :"#{@attrname}=", @options[:default_value]
701 | end
702 | end
703 | true
704 | end
705 |
706 | # (to be overridden by subclasses) Extract and return the value
707 | # of the attribute this node is responsible for (@attrname) from
708 | # _xml_. If the implementation decides that the attribute value
709 | # is "unset" in _xml_, it should raise NoAttrValueSet in order
710 | # to initiate proper handling of possibly supplied :optional and
711 | # :default_value options (you may use #default_when_xpath_err
712 | # for this purpose).
713 | def extract_attr_value(xml)
714 | raise "abstract method called"
715 | end
716 | def obj_to_xml(obj,xml) # :nodoc:
717 | value = obj.send(:"#{@attrname}")
718 | if @options.has_key? :default_value
719 | unless value == @options[:default_value]
720 | set_attr_value(xml, value)
721 | end
722 | else
723 | if value == nil
724 | raise XML::MappingError, "no value, and no default value, for attribute: #{@attrname}"
725 | end
726 | set_attr_value(xml, value)
727 | end
728 | true
729 | end
730 | # (to be overridden by subclasses) Write _value_, which is the
731 | # current value of the attribute this node is responsible for
732 | # (@attrname), into (the correct sub-nodes, attributes,
733 | # whatever) of _xml_.
734 | def set_attr_value(xml, value)
735 | raise "abstract method called"
736 | end
737 | def obj_initializing(obj,mapping) # :nodoc:
738 | if @options.has_key?(:default_value) and (mapping==nil || mapping==@mapping)
739 | begin
740 | obj.send :"#{@attrname}=", @options[:default_value].clone
741 | rescue
742 | obj.send :"#{@attrname}=", @options[:default_value]
743 | end
744 | end
745 | end
746 | # utility method to be used by implementations of
747 | # #extract_attr_value. Calls the supplied block, catching
748 | # XML::XXPathError and mapping it to NoAttrValueSet. This is for
749 | # the common case that an implementation considers an attribute
750 | # value not to be present in the XML if some specific sub-path
751 | # does not exist.
752 | def default_when_xpath_err # :yields:
753 | begin
754 | yield
755 | rescue XML::XXPathError => err
756 | raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})"
757 | end
758 | end
759 | # (overridden) returns true if and only if the value of this
760 | # node's attribute in _obj_ is non-nil.
761 | def is_present_in? obj
762 | nil != obj.send(:"#{@attrname}")
763 | end
764 | end
765 |
766 | end
767 |
768 | end
769 |
--------------------------------------------------------------------------------
/lib/xml/mapping/core_classes_mapping.rb:
--------------------------------------------------------------------------------
1 | class String
2 | def self.load_from_xml(xml, options={:mapping=>:_default})
3 | xml.text
4 | end
5 |
6 | def fill_into_xml(xml, options={:mapping=>:_default})
7 | xml.text = self
8 | end
9 |
10 | def text
11 | self
12 | end
13 | end
14 |
15 |
16 | class Numeric
17 | def self.load_from_xml(xml, options={:mapping=>:_default})
18 | begin
19 | Integer(xml.text)
20 | rescue ArgumentError
21 | Float(xml.text)
22 | end
23 | end
24 |
25 | def fill_into_xml(xml, options={:mapping=>:_default})
26 | xml.text = self.to_s
27 | end
28 |
29 | def text
30 | self.to_s
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/xml/mapping/standard_nodes.rb:
--------------------------------------------------------------------------------
1 | # xml-mapping -- bidirectional Ruby-XML mapper
2 | # Copyright (C) 2004,2005 Olaf Klischat
3 |
4 | module XML
5 |
6 | module Mapping
7 |
8 | # Node factory function synopsis:
9 | #
10 | # text_node :_attrname_, _path_ [, :default_value=>_obj_]
11 | # [, :optional=>true]
12 | # [, :mapping=>_m_]
13 | #
14 | # Node that maps an XML node's text (the element's first child
15 | # text node resp. the attribute's value) to a (string) attribute
16 | # of the mapped object. _path_ (an XPath expression) locates the
17 | # XML node, _attrname_ (a symbol) names the
18 | # attribute. :default_value is the default value,
19 | # :optional=>true is equivalent to :default_value=>nil (see
20 | # superclass documentation for details). _m_ is the
21 | # mapping; it defaults to the current default mapping
22 | class TextNode < SingleAttributeNode
23 | def initialize(*args)
24 | path,*args = super(*args)
25 | @path = XML::XXPath.new(path)
26 | args
27 | end
28 | def extract_attr_value(xml) # :nodoc:
29 | default_when_xpath_err{ @path.first(xml).text }
30 | end
31 | def set_attr_value(xml, value) # :nodoc:
32 | @path.first(xml,:ensure_created=>true).text = value
33 | end
34 | end
35 |
36 | # Node factory function synopsis:
37 | #
38 | # numeric_node :_attrname_, _path_ [, :default_value=>_obj_]
39 | # [, :optional=>true]
40 | # [, :mapping=>_m_]
41 | #
42 | # Like TextNode, but interprets the XML node's text as a number
43 | # (Integer or Float, depending on the nodes's text) and maps it to
44 | # an Integer or Float attribute.
45 | class NumericNode < SingleAttributeNode
46 | def initialize(*args)
47 | path,*args = super(*args)
48 | @path = XML::XXPath.new(path)
49 | args
50 | end
51 |
52 | def extract_attr_value(xml) # :nodoc:
53 | txt = default_when_xpath_err{ @path.first(xml).text }
54 |
55 | if txt.nil?
56 | raise NoAttrValueSet, "Attribute #{@attrname} not set (text missing)"
57 | else
58 | begin
59 | Integer(txt)
60 | rescue ArgumentError
61 | Float(txt)
62 | end
63 | end
64 | end
65 |
66 | def set_attr_value(xml, value) # :nodoc:
67 | raise RuntimeError, "Not an integer: #{value}" unless Numeric===value
68 | @path.first(xml,:ensure_created=>true).text = value.to_s
69 | end
70 | end
71 |
72 | # (does somebody have a better name for this class?) base node
73 | # class that provides an initializer which lets the user specify a
74 | # means to marshal/unmarshal a Ruby object to/from XML. Used as
75 | # the base class for nodes that map some sub-nodes of their XML
76 | # tree to (Ruby-)sub-objects of their attribute.
77 | class SubObjectBaseNode < SingleAttributeNode
78 | # processes the keyword arguments :class, :marshaller, and
79 | # :unmarshaller (_args_ is ignored). When this initiaizer
80 | # returns, @marshaller and @unmarshaller are set to procs that
81 | # marshal/unmarshal a Ruby object to/from an XML tree according
82 | # to the keyword arguments that were passed to the initializer:
83 | #
84 | # You either supply a :class argument with a class implementing
85 | # XML::Mapping -- in that case, the subtree will be mapped to an
86 | # instance of that class (using load_from_xml
87 | # resp. fill_into_xml). Or, you supply :marshaller and
88 | # :unmarshaller arguments specifying explicit
89 | # unmarshaller/marshaller procs. The :marshaller proc takes
90 | # arguments _xml_,_value_ and must fill _value_ (the object to
91 | # be marshalled) into _xml_; the :unmarshaller proc takes _xml_
92 | # and must extract and return the object value from it. Or, you
93 | # specify none of those arguments, in which case the name of the
94 | # class to create will be automatically deduced from the root
95 | # element name of the XML node (see
96 | # XML::Mapping::load_object_from_xml,
97 | # XML::Mapping::class_for_root_elt_name).
98 | #
99 | # If both :class and :marshaller/:unmarshaller arguments are
100 | # supplied, the latter take precedence.
101 | def initialize(*args)
102 | args = super(*args)
103 |
104 | @sub_mapping = @options[:sub_mapping] || @mapping
105 | @marshaller, @unmarshaller = @options[:marshaller], @options[:unmarshaller]
106 |
107 | if @options[:class]
108 | unless @marshaller
109 | @marshaller = proc {|xml,value|
110 | value.fill_into_xml xml, :mapping=>@sub_mapping
111 | if xml.unspecified?
112 | xml.name = value.class.root_element_name :mapping=>@sub_mapping
113 | xml.unspecified = false
114 | end
115 | }
116 | end
117 | unless @unmarshaller
118 | @unmarshaller = proc {|xml|
119 | @options[:class].load_from_xml xml, :mapping=>@sub_mapping
120 | }
121 | end
122 | end
123 |
124 | unless @marshaller
125 | @marshaller = proc {|xml,value|
126 | value.fill_into_xml xml, :mapping=>@sub_mapping
127 | if xml.unspecified?
128 | xml.name = value.class.root_element_name :mapping=>@sub_mapping
129 | xml.unspecified = false
130 | end
131 | }
132 | end
133 | unless @unmarshaller
134 | @unmarshaller = proc {|xml|
135 | XML::Mapping.load_object_from_xml xml, :mapping=>@sub_mapping
136 | }
137 | end
138 |
139 | args
140 | end
141 | end
142 |
143 | require 'xml/mapping/core_classes_mapping'
144 |
145 | # Node factory function synopsis:
146 | #
147 | # object_node :_attrname_, _path_ [, :default_value=>_obj_]
148 | # [, :optional=>true]
149 | # [, :class=>_c_]
150 | # [, :marshaller=>_proc_]
151 | # [, :unmarshaller=>_proc_]
152 | # [, :mapping=>_m_]
153 | # [, :sub_mapping=>_sm_]
154 | #
155 | # Node that maps a subtree in the source XML to a Ruby
156 | # object. :_attrname_ and _path_ are again the attribute name
157 | # resp. XPath expression of the mapped attribute; the keyword
158 | # arguments :default_value and :optional are
159 | # handled by the SingleAttributeNode superclass. The XML subnode
160 | # named by _path_ is mapped to the attribute named by :_attrname_
161 | # according to the keyword arguments :class,
162 | # :marshaller, and :unmarshaller, which are
163 | # handled by the SubObjectBaseNode superclass.
164 | class ObjectNode < SubObjectBaseNode
165 | # Initializer. _path_ (a string denoting an XPath expression) is
166 | # the location of the subtree.
167 | def initialize(*args)
168 | path,*args = super(*args)
169 | @path = XML::XXPath.new(path)
170 | args
171 | end
172 | def extract_attr_value(xml) # :nodoc:
173 | @unmarshaller.call(default_when_xpath_err{@path.first(xml)})
174 | end
175 | def set_attr_value(xml, value) # :nodoc:
176 | @marshaller.call(@path.first(xml,:ensure_created=>true), value)
177 | end
178 | end
179 |
180 | # Node factory function synopsis:
181 | #
182 | # boolean_node :_attrname_, _path_,
183 | # _true_value_, _false_value_ [, :default_value=>_obj_]
184 | # [, :optional=>true]
185 | # [, :mapping=>_m_]
186 | #
187 | # Node that maps an XML node's text (the element name resp. the
188 | # attribute value) to a boolean attribute of the mapped
189 | # object. The attribute named by :_attrname_ is mapped to/from the
190 | # XML subnode named by the XPath expression _path_. _true_value_
191 | # is the text the node must have in order to represent the +true+
192 | # boolean value, _false_value_ (actually, any value other than
193 | # _true_value_) is the text the node must have in order to
194 | # represent the +false+ boolean value.
195 | class BooleanNode < SingleAttributeNode
196 | # Initializer.
197 | def initialize(*args)
198 | path,true_value,false_value,*args = super(*args)
199 | @path = XML::XXPath.new(path)
200 | @true_value = true_value; @false_value = false_value
201 | args
202 | end
203 | def extract_attr_value(xml) # :nodoc:
204 | default_when_xpath_err{ @path.first(xml).text==@true_value }
205 | end
206 | def set_attr_value(xml, value) # :nodoc:
207 | @path.first(xml,:ensure_created=>true).text = value ? @true_value : @false_value
208 | end
209 | end
210 |
211 | # Node factory function synopsis:
212 | #
213 | # array_node :_attrname_, _per_arrelement_path_
214 | # [, :default_value=>_obj_]
215 | # [, :optional=>true]
216 | # [, :class=>_c_]
217 | # [, :marshaller=>_proc_]
218 | # [, :unmarshaller=>_proc_]
219 | # [, :mapping=>_m_]
220 | # [, :sub_mapping=>_sm_]
221 | #
222 | # -or-
223 | #
224 | # array_node :_attrname_, _base_path_, _per_arrelement_path_
225 | # [keyword args the same]
226 | #
227 | # Node that maps a sequence of sub-nodes of the XML tree to an
228 | # attribute containing an array of Ruby objects, with each array
229 | # element mapping to a corresponding member of the sequence of
230 | # sub-nodes.
231 | #
232 | # If _base_path_ is not supplied, it is assumed to be
233 | # "". _base_path_+"/"+_per_arrelement_path_ is an XPath
234 | # expression that must "yield" the sequence of XML nodes that is
235 | # to be mapped to the array. The difference between _base_path_
236 | # and _per_arrelement_path_ becomes important when marshalling the
237 | # array attribute back to XML. When that happens, _base_path_
238 | # names the most specific common parent node of all the mapped
239 | # sub-nodes, and _per_arrelement_path_ names (relative to
240 | # _base_path_) the part of the path that is duplicated for each
241 | # array element. For example, with _base_path_=="foo/bar"
242 | # and _per_arrelement_path_=="hi/ho", an array
243 | # [x,y,z] will be written to an XML structure that looks
244 | # like this:
245 | #
246 | #
247 | #
248 | #
249 | #
250 | # [marshalled object x]
251 | #
252 | #
253 | #
254 | #
255 | # [marshalled object y]
256 | #
257 | #
258 | #
259 | #
260 | # [marshalled object z]
261 | #
262 | #
263 | #
264 | #
265 | class ArrayNode < SubObjectBaseNode
266 | # Initializer. Called with keyword arguments and either 1 or 2
267 | # paths; the hindmost path argument passed is delegated to
268 | # _per_arrelement_path_; the preceding path argument (if
269 | # present, "" by default) is delegated to _base_path_.
270 | def initialize(*args)
271 | path,path2,*args = super(*args)
272 | base_path,per_arrelement_path = if path2
273 | [path,path2]
274 | else
275 | [".",path]
276 | end
277 | per_arrelement_path=per_arrelement_path[1..-1] if per_arrelement_path[0]==?/
278 | @base_path = XML::XXPath.new(base_path)
279 | @per_arrelement_path = XML::XXPath.new(per_arrelement_path)
280 | @reader_path = XML::XXPath.new(base_path+"/"+per_arrelement_path)
281 | args
282 | end
283 | def extract_attr_value(xml) # :nodoc:
284 | result = []
285 | default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
286 | result << @unmarshaller.call(elt)
287 | end
288 | result
289 | end
290 | def set_attr_value(xml, value) # :nodoc:
291 | base_elt = @base_path.first(xml,:ensure_created=>true)
292 | value.each do |arr_elt|
293 | @marshaller.call(@per_arrelement_path.create_new(base_elt), arr_elt)
294 | end
295 | end
296 | end
297 |
298 |
299 | # Node factory function synopsis:
300 | #
301 | # hash_node :_attrname_, _per_hashelement_path_, _key_path_
302 | # [, :default_value=>_obj_]
303 | # [, :optional=>true]
304 | # [, :class=>_c_]
305 | # [, :marshaller=>_proc_]
306 | # [, :unmarshaller=>_proc_]
307 | # [, :mapping=>_m_]
308 | # [, :sub_mapping=>_sm_]
309 | #
310 | # - or -
311 | #
312 | # hash_node :_attrname_, _base_path_, _per_hashelement_path_, _key_path_
313 | # [keyword args the same]
314 | #
315 | # Node that maps a sequence of sub-nodes of the XML tree to an
316 | # attribute containing a hash of Ruby objects, with each hash
317 | # value mapping to a corresponding member of the sequence of
318 | # sub-nodes. The (string-valued) hash key associated with a hash
319 | # value _v_ is mapped to the text of a specific sub-node of _v_'s
320 | # sub-node.
321 | #
322 | # Analogously to ArrayNode, _base_path_ and _per_arrelement_path_
323 | # define the XPath expression that "yields" the sequence of XML
324 | # nodes, each of which maps to a value in the hash table. Relative
325 | # to such a node, key_path_ names the node whose text becomes the
326 | # associated hash key.
327 | class HashNode < SubObjectBaseNode
328 | # Initializer. Called with keyword arguments and either 2 or 3
329 | # paths; the hindmost path argument passed is delegated to
330 | # _key_path_, the preceding path argument is delegated to
331 | # _per_arrelement_path_, the path preceding that argument (if
332 | # present, "" by default) is delegated to _base_path_. The
333 | # meaning of the keyword arguments is the same as for
334 | # ObjectNode.
335 | def initialize(*args)
336 | path1,path2,path3,*args = super(*args)
337 | base_path,per_hashelement_path,key_path = if path3
338 | [path1,path2,path3]
339 | else
340 | ["",path1,path2]
341 | end
342 | per_hashelement_path=per_hashelement_path[1..-1] if per_hashelement_path[0]==?/
343 | @base_path = XML::XXPath.new(base_path)
344 | @per_hashelement_path = XML::XXPath.new(per_hashelement_path)
345 | @key_path = XML::XXPath.new(key_path)
346 | @reader_path = XML::XXPath.new(base_path+"/"+per_hashelement_path)
347 | args
348 | end
349 | def extract_attr_value(xml) # :nodoc:
350 | result = {}
351 | default_when_xpath_err{@reader_path.all(xml)}.each do |elt|
352 | key = @key_path.first(elt).text
353 | value = @unmarshaller.call(elt)
354 | result[key] = value
355 | end
356 | result
357 | end
358 | def set_attr_value(xml, value) # :nodoc:
359 | base_elt = @base_path.first(xml,:ensure_created=>true)
360 | value.each_pair do |k,v|
361 | elt = @per_hashelement_path.create_new(base_elt)
362 | @marshaller.call(elt,v)
363 | @key_path.first(elt,:ensure_created=>true).text = k
364 | end
365 | end
366 | end
367 |
368 |
369 | class ChoiceNode < Node
370 |
371 | def initialize(*args)
372 | args = super(*args)
373 | @choices = []
374 | path=nil
375 | args.each do |arg|
376 | next if [:if,:then,:elsif].include? arg
377 | if path.nil?
378 | path = (if [:else,:default,:otherwise].include? arg
379 | :else
380 | else
381 | XML::XXPath.new arg
382 | end)
383 | else
384 | raise XML::MappingError, "node expected, found: #{arg.inspect}" unless Node===arg
385 | @choices << [path,arg]
386 |
387 | # undo what the node factory fcn did -- ugly ugly! would
388 | # need some way to lazy-evaluate arg (a proc would be
389 | # simple but ugly for the user), and then use some
390 | # mechanism (a flag with dynamic scope probably) to tell
391 | # the node factory fcn not to add the node to the
392 | # xml_mapping_nodes
393 | @owner.xml_mapping_nodes(:mapping=>@mapping).delete arg
394 | path=nil
395 | end
396 | end
397 |
398 | raise XML::MappingError, "node missing at end of argument list" unless path.nil?
399 | raise XML::MappingError, "no choices were supplied" if @choices.empty?
400 |
401 | []
402 | end
403 |
404 | def xml_to_obj(obj,xml)
405 | @choices.each do |path,node|
406 | if path==:else or not(path.all(xml).empty?)
407 | node.xml_to_obj(obj,xml)
408 | return true
409 | end
410 | end
411 | raise XML::MappingError, "xml_to_obj: no choice matched in: #{xml}"
412 | end
413 |
414 | def obj_to_xml(obj,xml)
415 | @choices.each do |path,node|
416 | if node.is_present_in? obj
417 | node.obj_to_xml(obj,xml)
418 | path.first(xml, :ensure_created=>true)
419 | return true
420 | end
421 | end
422 | # @choices[0][1].obj_to_xml(obj,xml)
423 | raise XML::MappingError, "obj_to_xml: no choice present in object: #{obj.inspect}"
424 | end
425 |
426 | def obj_initializing(obj,mapping)
427 | @choices[0][1].obj_initializing(obj,mapping)
428 | end
429 |
430 | # (overridden) true if at least one of our nodes is_present_in?
431 | # obj.
432 | def is_present_in? obj
433 | # TODO: use Enumerable#any?
434 | @choices.inject(false){|prev,(path,node)| prev or node.is_present_in?(obj)}
435 | end
436 | end
437 |
438 | end
439 |
440 | end
441 |
--------------------------------------------------------------------------------
/lib/xml/mapping/version.rb:
--------------------------------------------------------------------------------
1 | # xml-mapping -- bidirectional Ruby-XML mapper
2 | # Copyright (C) 2004-2010 Olaf Klischat
3 |
4 | module XML
5 | module Mapping
6 | VERSION = '0.10.1'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/xml/rexml_ext.rb:
--------------------------------------------------------------------------------
1 | # xxpath -- XPath implementation for Ruby, including write access
2 | # Copyright (C) 2004-2010 Olaf Klischat
3 |
4 | require 'rexml/document'
5 |
6 | module XML
7 |
8 | class XXPath
9 | module Accessors #:nodoc:
10 |
11 | # we need a boolean "unspecified?" attribute for XML nodes --
12 | # paths like "*" oder (somewhen) "foo|bar" create "unspecified"
13 | # nodes that the user must then "specify" by setting their text
14 | # etc. (or manually setting unspecified=false)
15 | #
16 | # This is mixed into the REXML::Element and
17 | # XML::XXPath::Accessors::Attribute classes.
18 | module UnspecifiednessSupport
19 |
20 | def unspecified?
21 | @xml_xpath_unspecified ||= false
22 | end
23 |
24 | def unspecified=(x)
25 | @xml_xpath_unspecified = x
26 | end
27 |
28 | def self.append_features(base)
29 | return if base.included_modules.include? self # avoid aliasing methods more than once
30 | # (would lead to infinite recursion)
31 | super
32 | base.module_eval <<-EOS
33 | alias_method :_text_orig, :text
34 | alias_method :_textis_orig, :text=
35 | def text
36 | # we're suffering from the "fragile base class"
37 | # phenomenon here -- we don't know whether the
38 | # implementation of the class we get mixed into always
39 | # calls text (instead of just accessing @text or so)
40 | if unspecified?
41 | "[UNSPECIFIED]"
42 | else
43 | _text_orig
44 | end
45 | end
46 | def text=(x)
47 | _textis_orig(x)
48 | self.unspecified=false
49 | end
50 |
51 | alias_method :_nameis_orig, :name=
52 | def name=(x)
53 | _nameis_orig(x)
54 | self.unspecified=false
55 | end
56 | EOS
57 | end
58 |
59 | end
60 |
61 | class REXML::Element #:nodoc:
62 | include UnspecifiednessSupport
63 | end
64 |
65 | # attribute node, more or less call-compatible with REXML's
66 | # Element. REXML's Attribute class doesn't provide this...
67 | #
68 | # The all/first calls return instances of this class if they
69 | # matched an attribute node.
70 | class Attribute
71 | attr_reader :parent, :name
72 | attr_writer :name
73 |
74 | def initialize(parent,name)
75 | @parent,@name = parent,name
76 | end
77 |
78 | def self.new(parent,name,create)
79 | if parent.attributes[name]
80 | super(parent,name)
81 | else
82 | if create
83 | parent.attributes[name] = "[unset]"
84 | super(parent,name)
85 | else
86 | nil
87 | end
88 | end
89 | end
90 |
91 | # the value of the attribute.
92 | def text
93 | parent.attributes[@name]
94 | end
95 |
96 | def text=(x)
97 | parent.attributes[@name] = x
98 | end
99 |
100 | def ==(other)
101 | other.kind_of?(Attribute) and other.parent==parent and other.name==name
102 | end
103 |
104 | include UnspecifiednessSupport
105 | end
106 | end
107 |
108 | end
109 |
110 | end
111 |
112 |
113 |
114 |
115 |
116 |
117 | class REXML::Parent
118 | def each_on_axis_child
119 | if respond_to? :attributes
120 | attributes.each_key do |name|
121 | yield XML::XXPath::Accessors::Attribute.new(self, name, false)
122 | end
123 | end
124 | each_child do |c|
125 | yield c
126 | end
127 | end
128 |
129 | def each_on_axis_descendant(&block)
130 | each_on_axis_child do |c|
131 | block.call c
132 | if REXML::Parent===c
133 | c.each_on_axis_descendant(&block)
134 | end
135 | end
136 | end
137 |
138 | def each_on_axis_self
139 | yield self
140 | end
141 |
142 | def each_on_axis(axis, &block)
143 | send :"each_on_axis_#{axis}", &block
144 | end
145 | end
146 |
147 |
148 | ## hotfix for REXML bug #128 -- see http://trac.germane-software.com/rexml/ticket/128
149 | # a working Element#write is required by several tests and
150 | # documentation code snippets
151 | begin
152 | # temporarily suppress warnings
153 | class < -1
174 | if transitive
175 | require "rexml/formatters/transitive"
176 | REXML::Formatters::Transitive.new( indent, ie_hack )
177 | else
178 | REXML::Formatters::Pretty.new( indent, ie_hack )
179 | end
180 | else
181 | REXML::Formatters::Default.new( ie_hack )
182 | end
183 | formatter.write( self, output )
184 | end
185 | end
186 | end
187 |
--------------------------------------------------------------------------------
/lib/xml/xxpath.rb:
--------------------------------------------------------------------------------
1 | # xxpath -- XPath implementation for Ruby, including write access
2 | # Copyright (C) 2004-2006 Olaf Klischat
3 |
4 | require 'rexml/document'
5 | require 'xml/rexml_ext'
6 | require 'xml/xxpath/steps'
7 |
8 | module XML
9 |
10 | class XXPathError < RuntimeError
11 | end
12 |
13 | # Instances of this class hold (in a pre-compiled form) an XPath
14 | # pattern. You call instance methods like +each+, +first+, +all+,
15 | # create_new on instances of this class to apply the
16 | # pattern to REXML elements.
17 | class XXPath
18 |
19 | # create and compile a new XPath. _xpathstr_ is the string
20 | # representation (XPath pattern) of the path
21 | def initialize(xpathstr)
22 | @xpathstr = xpathstr # for error messages
23 |
24 | # TODO: write a real XPath parser sometime
25 |
26 | xpathstr='/'+xpathstr if xpathstr[0] != ?/
27 |
28 | @creator_procs = [ proc{|node,create_new| node} ]
29 | @reader_proc = proc {|nodes| nodes}
30 |
31 | part=nil; part_expected=true
32 | xpathstr.split(/(\/+)/)[1..-1].reverse.each do |x|
33 | if part_expected
34 | part=x
35 | part_expected = false
36 | next
37 | end
38 | part_expected = true
39 | axis = case x
40 | when '/'
41 | :child
42 | when '//'
43 | :descendant
44 | else
45 | raise XXPathError, "XPath (#{xpathstr}): unknown axis: #{x}"
46 | end
47 | axis=:self if axis==:child and (part[0]==?. or part=~/^self::/) # yuck
48 |
49 | step = Step.compile(axis,part)
50 | @creator_procs << step.creator(@creator_procs[-1])
51 | @reader_proc = step.reader(@reader_proc, @creator_procs[-1])
52 | end
53 | end
54 |
55 |
56 | # loop over all sub-nodes of _node_ that match this XPath.
57 | def each(node,options={},&block)
58 | all(node,options).each(&block)
59 | end
60 |
61 | # the first sub-node of _node_ that matches this XPath. If nothing
62 | # matches, raise XXPathError unless :allow_nil=>true was provided.
63 | #
64 | # If :ensure_created=>true is provided, first() ensures that a
65 | # match exists in _node_, creating one if none existed before.
66 | #
67 | # path.first(node,:create_new=>true) is equivalent
68 | # to path.create_new(node).
69 | def first(node,options={})
70 | a=all(node,options)
71 | if a.empty?
72 | if options[:allow_nil]
73 | nil
74 | else
75 | raise XXPathError, "path not found: #{@xpathstr}"
76 | end
77 | else
78 | a[0]
79 | end
80 | end
81 |
82 | # Return an Enumerable with all sub-nodes of _node_ that match
83 | # this XPath. Returns an empty Enumerable if no match was found.
84 | #
85 | # If :ensure_created=>true is provided, all() ensures that a match
86 | # exists in _node_, creating one (and returning it as the sole
87 | # element of the returned enumerable) if none existed before.
88 | def all(node,options={})
89 | raise "options not a hash" unless Hash===options
90 | if options[:create_new]
91 | return [ @creator_procs[-1].call(node,true) ]
92 | else
93 | last_nodes,rest_creator = catch(:not_found) do
94 | return @reader_proc.call([node])
95 | end
96 | if options[:ensure_created]
97 | [ rest_creator.call(last_nodes[0],false) ]
98 | else
99 | []
100 | end
101 | end
102 | end
103 |
104 | # create a completely new match of this XPath in
105 | # base_node. "Completely new" means that a new node will be
106 | # created for each path element, even if a matching node already
107 | # existed in base_node.
108 | #
109 | # path.create_new(node) is equivalent to
110 | # path.first(node,:create_new=>true).
111 | def create_new(base_node)
112 | first(base_node,:create_new=>true)
113 | end
114 |
115 | end
116 |
117 | end
118 |
--------------------------------------------------------------------------------
/lib/xml/xxpath/steps.rb:
--------------------------------------------------------------------------------
1 | # xxpath -- XPath implementation for Ruby, including write access
2 | # Copyright (C) 2004-2006 Olaf Klischat
3 |
4 | module XML
5 | class XXPath
6 |
7 | # base class for XPath "steps". Steps contain an "axis" (e.g.
8 | # "/", "//", i.e. the "child" resp. "descendants" axis), and
9 | # a "node matcher" like "foo" or "@bar" or "foo[@bar='baz']", i.e. they
10 | # match some XML nodes and don't match others (e.g. "foo[@bar='baz']"
11 | # macthes all XML element nodes named "foo" that contain an attribute
12 | # with name "bar" and value "baz").
13 | #
14 | # Steps can find out whether they match a given XML node
15 | # (Step#matches?(node)), and they know how to create a matchingnode
16 | # on a given base node (Step#create_on(node,create_new)).
17 | class Step #:nodoc:
18 | def self.inherited(subclass)
19 | (@subclasses||=[]) << subclass
20 | end
21 |
22 | # create and return an instance of the right Step subclass for
23 | # axis _axis_ (:child or :descendant atm.) and node matcher _string_
24 | def self.compile axis, string
25 | (@subclasses||=[]).each do |sc|
26 | obj = sc.compile axis, string
27 | return obj if obj
28 | end
29 | raise XXPathError, "can't compile XPath step: #{string}"
30 | end
31 |
32 | def initialize axis
33 | @axis = axis
34 | end
35 |
36 | # return a proc that takes a list of nodes, finds all sub-nodes
37 | # that are reachable from one of those nodes via _self_'s axis
38 | # and match (see below) _self_, and calls _prev_reader_ on them
39 | # (and returns the result). When the proc doesn't find any such
40 | # nodes, it throws :not_found,
41 | # [nodes,creator_from_here].
42 | #
43 | # Needed for compiling whole XPath expressions for reading.
44 | #
45 | # Step itself provides a generic default implementation
46 | # which checks whether _self_ matches a given node by calling
47 | # self.matches?(node). Subclasses must either implement such a
48 | # _matches?_ method or override _reader_ to provide more
49 | # specialized implementations for better performance.
50 | def reader(prev_reader,creator_from_here)
51 | proc {|nodes|
52 | next_nodes = []
53 | nodes.each do |node|
54 | if node.respond_to? :each_on_axis
55 | node.each_on_axis(@axis) do |subnode|
56 | next_nodes << subnode if self.matches?(subnode)
57 | end
58 | end
59 | end
60 | if (next_nodes.empty?)
61 | throw :not_found, [nodes,creator_from_here]
62 | else
63 | prev_reader.call(next_nodes)
64 | end
65 | }
66 | end
67 |
68 | # return a proc that takes a node, creates a sub-node matching
69 | # _self_ on it, and then calls _prev_creator_ on that and
70 | # returns the result.
71 | #
72 | # Needed for compiling whole XPath expressions for writing.
73 | #
74 | # Step itself provides a generic default
75 | # implementation, subclasses may provide specialized
76 | # implementations for better performance.
77 | def creator(prev_creator)
78 | if @axis==:child or @axis==:self
79 | proc {|node,create_new|
80 | prev_creator.call(self.create_on(node,create_new),
81 | create_new)
82 | }
83 | else
84 | proc {|node,create_new|
85 | raise XXPathError, "can't create axis: #{@axis}"
86 | }
87 | end
88 | end
89 | end
90 |
91 |
92 | class AttrStep < Step #:nodoc:
93 | def self.compile axis, string
94 | /^(?:\.|self::\*)\[@(.*?)='(.*?)'\]$/ === string or return nil
95 | self.new axis,$1,$2
96 | end
97 |
98 | def initialize(axis,attr_name,attr_value)
99 | super(axis)
100 | @attr_name,@attr_value = attr_name,attr_value
101 | end
102 |
103 | def matches? node
104 | node.is_a?(REXML::Element) and node.attributes[@attr_name]==@attr_value
105 | end
106 |
107 | def create_on(node,create_new)
108 | if create_new
109 | raise XXPathError, "XPath: .[@'#{@attr_name}'='#{@attr_value}']: create_new but context node already exists"
110 | end
111 | # TODO: raise if node.attributes[@attr_name] already exists?
112 | node.attributes[@attr_name]=@attr_value
113 | node
114 | end
115 | end
116 |
117 |
118 | class NameAndAttrStep < Step #:nodoc:
119 | def self.compile axis, string
120 | /^(.*?)\[@(.*?)='(.*?)'\]$/ === string or return nil
121 | self.new axis,$1,$2,$3
122 | end
123 |
124 | def initialize(axis,name,attr_name,attr_value)
125 | super(axis)
126 | @name,@attr_name,@attr_value = name,attr_name,attr_value
127 | end
128 |
129 | def matches? node
130 | node.is_a?(REXML::Element) and node.name==@name and node.attributes[@attr_name]==@attr_value
131 | end
132 |
133 | def create_on(node,create_new)
134 | if create_new
135 | newnode = node.elements.add(@name)
136 | else
137 | newnode = node.elements.select{|elt| elt.name==@name and not(elt.attributes[@attr_name])}[0]
138 | if not(newnode)
139 | newnode = node.elements.add(@name)
140 | end
141 | end
142 | newnode.attributes[@attr_name]=@attr_value
143 | newnode
144 | end
145 | end
146 |
147 |
148 | class NameAndIndexStep < Step #:nodoc:
149 | def self.compile axis, string
150 | /^(.*?)\[(\d+)\]$/ === string or return nil
151 | self.new axis,$1,$2.to_i
152 | end
153 |
154 | def initialize(axis,name,index)
155 | super(axis)
156 | @name,@index = name,index
157 | end
158 |
159 | def matches? node
160 | raise XXPathError, "can't use #{@name}[#{@index}] on root node" if node.parent.nil?
161 | node == node.parent.elements.select{|elt| elt.name==@name}[@index-1]
162 | end
163 |
164 | def create_on(node,create_new)
165 | name_matches = node.elements.select{|elt| elt.name==@name}
166 | if create_new and (name_matches.size >= @index)
167 | raise XXPathError, "XPath: #{@name}[#{@index}]: create_new and element already exists"
168 | end
169 | newnode = name_matches[0]
170 | (@index-name_matches.size).times do
171 | newnode = node.elements.add @name
172 | end
173 | newnode
174 | end
175 |
176 | def reader(prev_reader,creator_from_here)
177 | if @axis==:child
178 | index = @index - 1
179 | proc {|nodes|
180 | next_nodes = []
181 | nodes.each do |node|
182 | byname=node.elements.select{|elt| elt.name==@name}
183 | next_nodes << byname[index] if indexnil
60 |
61 | array_node :entries, "entries1", "*"
62 | end
63 |
64 | class BMMapping < BM
65 | include XML::Mapping
66 |
67 | root_element_name 'bookmark1'
68 |
69 | text_node :name, "@bmname"
70 | numeric_node :last_changed, "@bmlast-changed", :default_value=>nil
71 |
72 | text_node :url, "url"
73 | object_node :refinement, "refinement", :default_value=>nil
74 | end
75 | end
76 |
77 |
78 | module Mapping2
79 | # TODO
80 | end
81 |
--------------------------------------------------------------------------------
/test/company.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | # forward declarations
4 | class Address; end
5 | class Office; end
6 | class Customer; end
7 | class Thing; end
8 |
9 |
10 | class Company
11 | include XML::Mapping
12 |
13 | text_node :name, "@name"
14 |
15 | object_node :address, "address", :class=>Address
16 |
17 | array_node :offices, "offices", "office", :class=>Office
18 | hash_node :customers, "customers", "customer", "@uid", :class=>Customer
19 |
20 | text_node :ent1, "arrtest/entry[1]"
21 | text_node :ent2, "arrtest/entry[2]"
22 | text_node :ent3, "arrtest/entry[3]"
23 |
24 | array_node :stuff, "stuff", "*"
25 | array_node :things, "stuff2", "thing", :class=>Thing
26 |
27 | object_node :test_default_value_identity, "dummy", :default_value => ["default"]
28 | end
29 |
30 |
31 | class Address
32 | include XML::Mapping
33 |
34 | text_node :city, "city"
35 | numeric_node :zip, "zip", :default_value=>12576
36 | text_node :street, "street", :optional=>true
37 | numeric_node :number, "number"
38 | end
39 |
40 |
41 | class Office
42 | include XML::Mapping
43 |
44 | text_node :speciality, "@speciality"
45 | boolean_node :classified, "classified", "yes", "no"
46 | # object_node :address, "address", :class=>Address
47 | object_node :address, "address",
48 | :marshaller=>proc {|xml,value| value.fill_into_xml(xml)},
49 | :unmarshaller=>proc {|xml| Address.load_from_xml(xml)}
50 | end
51 |
52 |
53 | class Customer
54 | include XML::Mapping
55 |
56 | text_node :uid, "@uid"
57 | text_node :name, "name"
58 | end
59 |
60 |
61 | class Thing
62 | include XML::Mapping
63 |
64 | choice_node 'name', (text_node :name, 'name'),
65 | '@name', (text_node :name, '@name'),
66 | :else, (text_node :name, '.')
67 | end
68 |
69 |
70 | class Names1
71 | include XML::Mapping
72 |
73 | choice_node :if, 'name', :then, (text_node :name, 'name'),
74 | :elsif, 'names/name', :then, (array_node :names, 'names', 'name', :class=>String)
75 | end
76 |
77 |
78 | class ReaderTest
79 | include XML::Mapping
80 |
81 | attr_accessor :read
82 |
83 | text_node :foo, "foo"
84 | text_node :foo2, "foo2", :reader=>proc{|obj,xml| (obj.read||=[]) << :foo2 }
85 | text_node :foo3, "foo3", :reader=>proc{|obj,xml,default|
86 | (obj.read||=[]) << :foo3
87 | default.call(obj,xml)
88 | }
89 | text_node :bar, "bar"
90 | end
91 |
92 |
93 | class WriterTest
94 | include XML::Mapping
95 |
96 | text_node :foo, "foo"
97 | text_node :foo2, "foo2", :writer=>proc{|obj,xml| e = xml.elements.add; e.name='quux'; e.text='dingdong2' }
98 | text_node :foo3, "foo3", :writer=>proc{|obj,xml,default|
99 | default.call(obj,xml)
100 | e = xml.elements.add; e.name='quux'; e.text='dingdong3'
101 | }
102 | text_node :bar, "bar"
103 | end
104 |
105 |
106 | class ReaderWriterProcVsLambdaTest
107 | include XML::Mapping
108 |
109 | attr_accessor :read, :written
110 |
111 | text_node :proc_2args, "proc_2args", :reader=>Proc.new{|obj,xml|
112 | (obj.read||=[]) << :proc_2args
113 | },
114 | :writer=>Proc.new{|obj,xml|
115 | (obj.written||=[]) << :proc_2args
116 | }
117 |
118 | text_node :proc_3args, "proc_3args", :reader=>Proc.new{|obj,xml,default|
119 | (obj.read||=[]) << :proc_3args
120 | default.call(obj,xml)
121 | },
122 | :writer=>Proc.new{|obj,xml,default|
123 | (obj.written||=[]) << :proc_3args
124 | default.call(obj,xml)
125 | }
126 |
127 |
128 | text_node :lambda_2args, "lambda_2args", :reader=>lambda{|obj,xml|
129 | (obj.read||=[]) << :lambda_2args
130 | },
131 | :writer=>lambda{|obj,xml|
132 | (obj.written||=[]) << :lambda_2args
133 | }
134 |
135 |
136 | text_node :lambda_3args, "lambda_3args", :reader=>lambda{|obj,xml,default|
137 | (obj.read||=[]) << :lambda_3args
138 | default.call(obj,xml)
139 | },
140 | :writer=>lambda{|obj,xml,default|
141 | (obj.written||=[]) << :lambda_3args
142 | default.call(obj,xml)
143 | }
144 |
145 |
146 | end
147 |
--------------------------------------------------------------------------------
/test/documents_folders.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | class Entry
4 | include XML::Mapping
5 |
6 | text_node :name, "name"
7 |
8 | def initialize(init_props={})
9 | super() #the super initialize (inherited from XML::Mapping) must be called
10 | init_props.entries.each do |name,value|
11 | self.send :"#{name}=", value
12 | end
13 | end
14 | end
15 |
16 |
17 | class Document []
34 |
35 | def [](name)
36 | entries.select{|e|e.name==name}[0]
37 | end
38 |
39 | def append(name,entry)
40 | entries << entry
41 | entry.name = name
42 | entry
43 | end
44 |
45 | def ==(other)
46 | Folder===other and
47 | other.name==self.name and
48 | other.entries==self.entries
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/examples_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 | $:.unshift File.dirname(__FILE__)+"/../examples"
3 |
4 | require 'test/unit'
5 | require 'xml/xxpath_methods'
6 |
7 | require 'xml/mapping'
8 |
9 | # unit tests for some code in ../examples. Most of that code is
10 | # included in ../README and tested when regenerating the README, but
11 | # some things are better done outside.
12 | class ExamplesTest < Test::Unit::TestCase
13 |
14 | def test_time_node
15 | require 'time_node'
16 | require 'order_signature_enhanced'
17 |
18 | s = Signature.load_from_file File.dirname(__FILE__)+"/../examples/order_signature_enhanced.xml"
19 | assert_equal Time.local(2005,2,13), s.signed_on
20 |
21 | s.signed_on = Time.local(2006,6,15)
22 | xml2 = s.save_to_xml
23 |
24 | assert_equal "15", xml2.first_xpath("signed-on/day").text
25 | assert_equal "6", xml2.first_xpath("signed-on/month").text
26 | assert_equal "2006", xml2.first_xpath("signed-on/year").text
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/test/fixtures/benchmark.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | x
5 | bar1
6 |
7 | y
8 |
9 |
10 | bar2
11 |
12 |
13 | quux1
14 |
15 |
16 | bar3
17 |
18 |
19 | bar4
20 |
21 | bar4-1
22 |
23 |
24 | z
25 |
26 | bar4-2
27 |
28 | hello
29 |
30 | bar4-3
31 |
32 |
33 |
34 | bar4-4
35 |
36 |
37 | bar4-5
38 |
39 |
40 | bar4-quux1
41 |
42 |
43 | bar4-6
44 |
45 |
46 | bar4-7
47 |
48 |
49 |
50 | quux2
51 |
52 | This buffer is for notes you don't want to save, and for Lisp
53 | evaluation. If you want to create a file, first visit that file
54 | with C-x C-f, then enter the text in that file's own buffer.
55 |
56 | bar5
57 |
58 |
59 | bar6
60 |
61 |
62 | quux3
63 |
64 |
65 | quux4
66 |
67 |
68 | bar7
69 |
70 |
71 | bar8
72 |
73 |
74 | bar9
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/test/fixtures/bookmarks1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | http://www.ruby-lang.org
7 |
8 |
9 |
10 | http://raa.ruby-lang.org/
11 |
12 |
13 |
14 |
15 |
16 | http://www.slashdot.org/
17 |
18 |
19 | http://www.groklaw.net/
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/company1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Berlin
7 | 10176
8 | Unter den Linden
9 | 12
10 |
11 |
12 |
13 |
14 | no
15 |
16 | Hamburg
17 | 18282
18 | Feldweg
19 | 28
20 |
21 |
22 |
23 | yes
24 |
25 | Baghdad
26 | Airport
27 | 18
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | James Kirk
37 | cash
38 |
39 |
40 |
41 | Ernie
42 | sweeties
43 |
44 |
45 |
46 | Cookie Monster
47 | cookies
48 |
49 |
50 |
51 | Saddam Hussein
52 | independent arms broker
53 |
54 |
55 |
56 |
57 |
58 |
59 | foo
60 | bar
61 | baz
62 |
63 |
64 |
65 |
66 |
67 | Saddam Hussein
68 | independent arms broker
69 |
70 |
71 | Berlin
72 | 10176
73 | Unter den Linden
74 | 12
75 |
76 |
77 | yes
78 |
79 | Baghdad
80 | Airport
81 | 18
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | name2
90 |
91 | name3
92 | name4-inlinename4-elt
93 |
94 |
95 |
--------------------------------------------------------------------------------
/test/fixtures/documents_folders.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | home
6 |
7 |
8 | plan
9 |
10 | inhale, exhale
11 | eat, drink, sleep
12 |
13 |
14 |
15 |
16 | work
17 |
18 |
19 | xml-mapping
20 |
21 |
22 | README
23 |
24 | XML-MAPPING: XML-to-object (and back) mapper
25 | for Ruby, including XPath interpreter
26 |
27 |
28 |
29 | Rakefile
30 |
31 | rake away
32 |
33 |
34 |
35 |
36 |
37 | schedule
38 |
39 | 12:00 AM lunch
40 | 4:00 PM appointment
41 |
42 |
43 |
44 |
45 |
46 |
47 | books
48 |
49 |
50 | The Hitchhiker's Guide to the Galaxy
51 |
52 | The Answer to Life, the Universe, and Everything
53 |
54 |
55 |
56 | Programming Ruby
57 |
58 | The Pragmatic Programmer's Guide
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/test/fixtures/documents_folders2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | home
5 |
6 |
7 | plan
8 | inhale, exhale
9 |
10 |
11 |
12 | work
13 |
14 |
15 | xml-mapping
16 |
17 |
18 | README
19 | foo bar baz
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/number.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/fixtures/triangle_m1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 3
6 | 0
7 |
8 |
9 | 2
10 | 4
11 |
12 |
13 | 0
14 | 1
15 |
16 | green
17 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/triangle_m2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tri1
5 |
6 |
7 | 3
8 | 0
9 |
10 |
11 | 2
12 | 4
13 |
14 |
15 | 0
16 | 1
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/inheritance_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 | require 'xml/mapping'
5 | require 'xml/xxpath_methods'
6 |
7 | class Base
8 | attr_accessor :baseinit
9 |
10 | def initialize(p)
11 | self.baseinit = p
12 | end
13 | end
14 |
15 | class Derived < Base
16 | include XML::Mapping
17 |
18 | text_node :mytext, "mytext"
19 | end
20 |
21 | class Derived2 < Base
22 | include XML::Mapping
23 |
24 | text_node :baseinit, "baseinit"
25 | end
26 |
27 | # test that tries to reproduce ticket #4783
28 | class InheritanceTest < Test::Unit::TestCase
29 |
30 | def test_inheritance_simple
31 | d = Derived.new "foo"
32 | assert_equal "foo", d.baseinit
33 | d.mytext = "hello"
34 | dxml=d.save_to_xml
35 | assert_equal "hello", dxml.first_xpath("mytext").text
36 | d2 = Derived.load_from_xml(dxml)
37 | assert_nil d2.baseinit
38 | assert_equal "hello", d2.mytext
39 | end
40 |
41 | def test_inheritance_superclass_initializing_mappedattr
42 | d = Derived2.new "foo"
43 | assert_equal "foo", d.baseinit
44 | dxml=d.save_to_xml
45 | assert_equal "foo", dxml.first_xpath("baseinit").text
46 | d2 = Derived2.load_from_xml(dxml)
47 | assert_equal "foo", d2.baseinit
48 | end
49 |
50 | end
51 |
--------------------------------------------------------------------------------
/test/multiple_mappings_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 | require 'triangle_mm'
5 |
6 | require 'xml/xxpath_methods'
7 |
8 | class MultipleMappingsTest < Test::Unit::TestCase
9 | def setup
10 | # need to undo mapping class definitions that may have been
11 | # established by other tests (and outlive those tests)
12 |
13 | # this requires some ugly hackery with internal variables
14 | XML::Mapping.module_eval <<-EOS
15 | Classes_by_rootelt_names.clear
16 | EOS
17 | Object.send(:remove_const, "Triangle")
18 | unless ($".delete "triangle_mm.rb")
19 | $".delete_if{|name| name =~ %r!test/triangle_mm.rb$!}
20 | end
21 | $:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb)
22 | require 'triangle_mm'
23 | end
24 |
25 | def test_read
26 | t1=Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1
27 | assert_raises(XML::MappingError) do
28 | Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m2
29 | end
30 | t2=Triangle.load_from_file File.dirname(__FILE__) + "/fixtures/triangle_m2.xml", :mapping=>:m2
31 |
32 | t=Triangle.new('tri1','green',
33 | Point.new(3,0),Point.new(2,4),Point.new(0,1))
34 |
35 | assert_equal t, t1
36 | assert_equal t, t2
37 | assert_equal t1, t2
38 | assert_equal "default description", t2.descr
39 | assert_nil t1.descr
40 | assert_not_equal Triangle.allocate, t
41 |
42 | # loading with default mapping should raise an exception because
43 | # the default mapping was never used yet
44 | assert_raises(XML::MappingError) do
45 | Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m1.xml")
46 | end
47 | assert_raises(XML::MappingError) do
48 | Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m2.xml")
49 | end
50 |
51 | # after using it once, we get empty objects
52 | Triangle.class_eval "use_mapping :_default"
53 | assert_equal Triangle.allocate,
54 | Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m1.xml")
55 | assert_equal Triangle.allocate,
56 | Triangle.load_from_file(File.dirname(__FILE__) + "/fixtures/triangle_m2.xml")
57 | end
58 |
59 |
60 | def test_read_polymorphic
61 | t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1
62 | t2=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m2.xml", :mapping=>:m2
63 | t=Triangle.new('tri1','green',
64 | Point.new(3,0),Point.new(2,4),Point.new(0,1))
65 |
66 | assert_equal t, t1
67 | assert_equal t, t2
68 | assert_equal t1, t2
69 | end
70 |
71 |
72 | def test_write
73 | t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1
74 | m1xml = t1.save_to_xml :mapping=>:m1
75 | m2xml = t1.save_to_xml :mapping=>:m2
76 |
77 | assert_equal t1.name, m1xml.first_xpath('@name').text
78 | assert_equal t1.name, m2xml.first_xpath('name').text
79 | assert_equal t1.p2, Point.load_from_xml(m1xml.first_xpath('pt2'), :mapping=>:m1)
80 | assert_equal t1.p2, Point.load_from_xml(m2xml.first_xpath('points/point[2]'), :mapping=>:m1)
81 | end
82 |
83 |
84 | def test_root_element
85 | t1=XML::Mapping.load_object_from_file File.dirname(__FILE__) + "/fixtures/triangle_m1.xml", :mapping=>:m1
86 | m1xml = t1.save_to_xml :mapping=>:m1
87 | m2xml = t1.save_to_xml :mapping=>:m2
88 |
89 | assert_equal "triangle", Triangle.root_element_name(:mapping=>:m1)
90 | assert_equal "triangle", Triangle.root_element_name(:mapping=>:m2)
91 | assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m1)
92 | assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m2)
93 | assert_equal "triangle", t1.save_to_xml(:mapping=>:m1).name
94 | assert_equal "triangle", t1.save_to_xml(:mapping=>:m2).name
95 |
96 | Triangle.class_eval <<-EOS
97 | use_mapping :m1
98 | root_element_name 'foobar'
99 | EOS
100 |
101 | assert_equal "foobar", Triangle.root_element_name(:mapping=>:m1)
102 | assert_equal "triangle", Triangle.root_element_name(:mapping=>:m2)
103 | assert_nil XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m1)
104 | assert_equal Triangle, XML::Mapping.class_for_root_elt_name("foobar",:mapping=>:m1)
105 | assert_equal Triangle, XML::Mapping.class_for_root_elt_name("triangle",:mapping=>:m2)
106 | assert_equal "foobar", t1.save_to_xml(:mapping=>:m1).name
107 | assert_equal "triangle", t1.save_to_xml(:mapping=>:m2).name
108 | assert_equal [Triangle,:m1], XML::Mapping.class_and_mapping_for_root_elt_name("foobar")
109 | assert_raises(XML::MappingError) do
110 | XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1)
111 | end
112 | assert_equal t1, XML::Mapping.load_object_from_xml(m2xml, :mapping=>:m2)
113 | m1xml.name = "foobar"
114 | assert_equal t1, XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1)
115 |
116 | Triangle.class_eval <<-EOS
117 | use_mapping :m1
118 | root_element_name 'triangle'
119 | EOS
120 |
121 | assert_raises(XML::MappingError) do
122 | XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1)
123 | end
124 | m1xml.name = "triangle"
125 | assert_equal t1, XML::Mapping.load_object_from_xml(m1xml, :mapping=>:m1)
126 | end
127 |
128 |
129 | def test_node_initialization
130 | end
131 |
132 |
133 | def test_misc
134 | m1nodes = Triangle.xml_mapping_nodes(:mapping=>:m1).map{|n|n.class}
135 | m2nodes = Triangle.xml_mapping_nodes(:mapping=>:m2).map{|n|n.class}
136 | allnodes = Triangle.xml_mapping_nodes.map{|n|n.class}
137 |
138 | t=XML::Mapping::TextNode
139 | o=XML::Mapping::ObjectNode
140 | assert_equal [t,o,o,t,o], m1nodes
141 | assert_equal [o,t,t,o,o,t], m2nodes
142 | begin
143 | assert_equal m1nodes+m2nodes, allnodes
144 | rescue Test::Unit::AssertionFailedError
145 | assert_equal m2nodes+m1nodes, allnodes
146 | end
147 |
148 | allm1nodes = Triangle.all_xml_mapping_nodes(:mapping=>:m1).map{|n|n.class}
149 | allm2nodes = Triangle.all_xml_mapping_nodes(:mapping=>:m2).map{|n|n.class}
150 | allallnodes = Triangle.all_xml_mapping_nodes.map{|n|n.class}
151 |
152 | assert_equal allm1nodes, m1nodes
153 | assert_equal allm2nodes, m2nodes
154 | assert_equal allnodes, allallnodes
155 | end
156 |
157 | end
158 |
--------------------------------------------------------------------------------
/test/number.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | class Number
4 | include XML::Mapping
5 |
6 | use_mapping :no_default
7 | numeric_node :value, 'value'
8 |
9 | use_mapping :with_default
10 | numeric_node :value, 'value', default_value: 0
11 | end
12 |
--------------------------------------------------------------------------------
/test/rexml_xpath_benchmark.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/benchmark_fixtures"
2 |
3 | require "rexml/xpath"
4 |
5 | require 'benchmark'
6 | include Benchmark
7 |
8 | rootelt = @d.root
9 | foo2elt = rootelt.elements[3]
10 | res1=res2=res3=res4=res5=nil
11 | print "(#{@count} runs)\n"
12 | bmbm(12) do |x|
13 | x.report("by_name") { @count.times { res1 = XPath.first(rootelt, @path_by_name) } }
14 | x.report("by_idx") { @count.times { res2 = XPath.first(rootelt, @path_by_idx) } }
15 | x.report("by_idx_idx") { @count.times { res3 = XPath.first(rootelt, @path_by_idx_idx) } }
16 | x.report("by_attr_idx") { @count.times { res4 = XPath.first(rootelt, @path_by_attr_idx) } }
17 | x.report("xxpath_by_attr") { (@count*4).times { res5 = XPath.first(foo2elt, @path_by_attr) } }
18 | end
19 |
20 |
21 | def assert_equal(expected,actual)
22 | expected==actual or raise "expected: #{expected.inspect}, actual: #{actual.inspect}"
23 | end
24 |
25 | assert_equal "bar4-2", res1.text.strip
26 | assert_equal "bar6", res2.text.strip
27 | assert_equal "bar4-6", res3.text.strip
28 | assert_equal "bar4-6", res4.text.strip
29 | assert_equal "xy", res5.value.strip
30 |
--------------------------------------------------------------------------------
/test/tests_init.rb:
--------------------------------------------------------------------------------
1 | $:.unshift File.dirname(__FILE__)
2 | $:.unshift File.dirname(__FILE__)+"/../lib"
3 |
--------------------------------------------------------------------------------
/test/triangle_mm.rb:
--------------------------------------------------------------------------------
1 | require 'xml/mapping'
2 |
3 | module XML::Mapping
4 | def ==(other)
5 | Marshal.dump(self) == Marshal.dump(other)
6 | end
7 | end
8 |
9 |
10 | # forward declarations
11 | class Point; end
12 |
13 | class Triangle
14 | include XML::Mapping
15 |
16 | use_mapping :m1
17 |
18 | text_node :name, "@name"
19 | object_node :p1, "pt1", :class=>Point
20 | object_node :p2, "points/point[2]", :class=>Point, :mapping=>:m2, :sub_mapping=>:m1
21 | object_node :p3, "pt3", :class=>Point
22 | text_node :color, "color"
23 |
24 |
25 | use_mapping :m2
26 |
27 | text_node :color, "@color"
28 | text_node :name, "name"
29 | object_node :p1, "points/point[1]", :class=>Point, :sub_mapping=>:m1
30 | object_node :p2, "pt2", :class=>Point, :mapping=>:m1
31 | object_node :p3, "points/point[3]", :class=>Point, :sub_mapping=>:m1
32 |
33 | text_node :descr, "description", :default_value=>"default description"
34 |
35 | def initialize(name,color,p1,p2,p3)
36 | @name,@color,@p1,@p2,@p3 = name,color,p1,p2,p3
37 | end
38 |
39 | def ==(other)
40 | name==other.name and color==other.color and
41 | p1==other.p1 and p2==other.p2 and p3==other.p3
42 | end
43 | end
44 |
45 |
46 | class Point
47 | include XML::Mapping
48 |
49 | use_mapping :m1
50 |
51 | numeric_node :x, "x"
52 | numeric_node :y, "y"
53 |
54 | def initialize(x,y)
55 | @x,@y = x,y
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/xml_mapping_adv_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 | require 'documents_folders'
5 | require 'bookmarks'
6 |
7 | class XmlMappingAdvancedTest < Test::Unit::TestCase
8 | def setup
9 | XML::Mapping.module_eval <<-EOS
10 | Classes_by_rootelt_names.clear
11 | EOS
12 | Object.send(:remove_const, "Document")
13 | Object.send(:remove_const, "Folder")
14 |
15 | unless ($".delete "documents_folders.rb") # works in 1.8 only. In 1.9, $" contains absolute paths.
16 | $".delete_if{|name| name =~ %r!test/documents_folders.rb$!}
17 | end
18 | unless ($".delete "bookmarks.rb")
19 | $".delete_if{|name| name =~ %r!test/bookmarks.rb$!}
20 | end
21 | require 'documents_folders'
22 | require 'bookmarks'
23 |
24 | @f_xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/documents_folders2.xml"))
25 | @f = XML::Mapping.load_object_from_xml(@f_xml.root)
26 |
27 | @bm1_xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/bookmarks1.xml"))
28 | @bm1 = XML::Mapping.load_object_from_xml(@bm1_xml.root)
29 | end
30 |
31 | def test_read_polymorphic_object
32 | expected = Folder.new \
33 | :name => "home",
34 | :entries => [
35 | Document.new(:name => "plan", :contents => " inhale, exhale"),
36 | Folder.new(:name => "work",
37 | :entries => [
38 | Folder.new(:name => "xml-mapping",
39 | :entries => [Document.new(:name => "README",
40 | :contents => "foo bar baz")]
41 | )
42 | ])
43 | ]
44 |
45 | assert_equal expected, @f
46 | end
47 |
48 | def test_write_polymorphic_object
49 | xml = @f.save_to_xml
50 | assert_equal "folder", xml.name
51 | assert_equal "home", xml.elements[1].text
52 | assert_equal "document", xml.elements[2].name
53 | assert_equal "folder", xml.elements[3].name
54 | assert_equal "name", xml.elements[3].elements[1].name
55 | assert_equal "folder", xml.elements[3].elements[2].name
56 | assert_equal "foo bar baz", xml.elements[3].elements[2].elements[2].elements[2].text
57 |
58 | @f.append "etc", Folder.new
59 | @f["etc"].append "passwd", Document.new
60 | @f["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh"
61 | @f["etc"].append "hosts", Document.new
62 | @f["etc"]["hosts"].contents = "127.0.0.1 localhost"
63 |
64 | xml = @f.save_to_xml
65 |
66 | xmletc = xml.elements[4]
67 | assert_equal "etc", xmletc.elements[1].text
68 | assert_equal "document", xmletc.elements[2].name
69 | assert_equal "passwd", xmletc.elements[2].elements[1].text
70 | assert_equal "foo:x:2:2:/bin/sh", xmletc.elements[2].elements[2].text
71 | end
72 |
73 | def test_read_bookmars1_2
74 | expected = BMFolder.new{|x|
75 | x.name = "root"
76 | x.last_changed = 123
77 | x.entries = [
78 | BM.new{|x|
79 | x.name="ruby"
80 | x.last_changed=345
81 | x.url="http://www.ruby-lang.org"
82 | x.refinement=nil
83 | },
84 | BM.new{|x|
85 | x.name="RAA"
86 | x.last_changed=nil
87 | x.url="http://raa.ruby-lang.org/"
88 | x.refinement=nil
89 | },
90 | BMFolder.new{|x|
91 | x.name="news"
92 | x.last_changed=nil
93 | x.entries = [
94 | BM.new{|x|
95 | x.name="/."
96 | x.last_changed=233
97 | x.url="http://www.slashdot.org/"
98 | x.refinement=nil
99 | },
100 | BM.new{|x|
101 | x.name="groklaw"
102 | x.last_changed=238
103 | x.url="http://www.groklaw.net/"
104 | x.refinement=nil
105 | }
106 | ]
107 | }
108 | ]
109 | }
110 | # need to compare expected==@bm1 because @bm1.== would be the
111 | # XML::Mapping#== defined in xml_mapping_test.rb ...
112 | assert_equal expected, @bm1
113 | assert_equal "root_set", @bm1.name_set
114 | assert_equal "ruby_set", @bm1.entries[0].name_set
115 | @bm1.entries[0].name = "foobar"
116 | assert_equal "foobar_set", @bm1.entries[0].name_set
117 | end
118 | end
119 |
120 |
--------------------------------------------------------------------------------
/test/xml_mapping_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 | require 'company'
5 | require 'xml/xxpath_methods'
6 |
7 | module XML::Mapping
8 | def ==(other)
9 | Marshal.dump(self) == Marshal.dump(other)
10 | end
11 | end
12 |
13 | class XmlMappingTest < Test::Unit::TestCase
14 | def setup
15 | # need to undo mapping class definitions that may have been
16 | # established by other tests (and outlive those tests)
17 |
18 | # this requires some ugly hackery with internal variables
19 | XML::Mapping.module_eval <<-EOS
20 | Classes_by_rootelt_names.clear
21 | EOS
22 | Object.send(:remove_const, "Company")
23 | Object.send(:remove_const, "Address")
24 | Object.send(:remove_const, "Office")
25 | Object.send(:remove_const, "Customer")
26 | Object.send(:remove_const, "Thing")
27 | Object.send(:remove_const, "Names1")
28 | Object.send(:remove_const, "ReaderTest")
29 | Object.send(:remove_const, "WriterTest")
30 | Object.send(:remove_const, "ReaderWriterProcVsLambdaTest")
31 | unless ($".delete "company.rb") # works in 1.8 only. In 1.9, $" contains absolute paths.
32 | $".delete_if{|name| name =~ %r!test/company.rb$!}
33 | end
34 | $:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb)
35 | require 'company'
36 |
37 | @xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/company1.xml"))
38 | @c = Company.load_from_xml(@xml.root)
39 | end
40 |
41 | def test_getter_text_node
42 | assert_equal "bar", @c.ent2
43 | end
44 |
45 | def test_getter_int_node
46 | assert_equal 18, @c.offices[1].address.number
47 | end
48 |
49 | def test_int_node_default_value
50 | require 'number'
51 | xml = REXML::Document.new(File.new(File.dirname(__FILE__) + "/fixtures/number.xml"))
52 |
53 | assert_raise(XML::MappingError.new('no value, and no default value: Attribute value not set (text missing)')) do
54 | Number.load_from_xml(xml.root, :mapping => :no_default)
55 | end
56 |
57 | num = nil
58 | assert_nothing_raised do
59 | num = Number.load_from_xml(xml.root, :mapping => :with_default)
60 | end
61 |
62 | assert_equal 0, num.value
63 | end
64 |
65 | def test_getter_boolean_node
66 | path=XML::XXPath.new("offices/office[2]/classified")
67 | assert_equal(path.first(@xml.root).text == "yes",
68 | @c.offices[1].classified)
69 | end
70 |
71 | def test_getter_hash_node
72 | assert_equal 4, @c.customers.keys.size
73 | ["cm", "ernie", "jim", "sad"].zip(@c.customers.keys.sort).each do |exp,ckey|
74 | assert_equal exp, ckey
75 | assert_equal exp, @c.customers[ckey].uid
76 | end
77 | end
78 |
79 |
80 | def test_getter_choice_node
81 | assert_equal 4, @c.things.size
82 | assert_equal "name1", @c.things[0].name
83 | assert_equal "name2", @c.things[1].name
84 | assert_equal "name3", @c.things[2].name
85 | assert_equal "name4-elt", @c.things[3].name
86 | end
87 |
88 |
89 | def test_getter_choice_node_multiple_attrs
90 | d = REXML::Document.new <<-EOS
91 |
92 |
93 | multi1
94 | multi2
95 |
96 | single
97 |
98 | EOS
99 | n1 = Names1.load_from_xml d.root
100 | assert_equal "single", n1.name
101 | assert_nil n1.names
102 |
103 | d.root.delete_element "name"
104 | n1 = Names1.load_from_xml d.root
105 | assert_nil n1.name
106 | assert_equal ["multi1","multi2"], n1.names
107 | end
108 |
109 |
110 | def test_choice_node_presence
111 | node = Thing.xml_mapping_nodes[0]
112 | t = Thing.new
113 | assert !(node.is_present_in? t)
114 | t.name = "Mary"
115 | assert node.is_present_in? t
116 | end
117 |
118 |
119 | def test_getter_array_node
120 | assert_equal ["pencils", "weapons of mass destruction"],
121 | @c.offices.map{|o|o.speciality}
122 | end
123 |
124 |
125 | def test_reader
126 | xml = REXML::Document.new("
127 | footext
128 | foo2text
129 | foo3text
130 | bartext
131 | ").root
132 | r = ReaderTest.load_from_xml xml
133 | assert_equal 'footext', r.foo
134 | assert_nil r.foo2
135 | assert_equal 'foo3text', r.foo3
136 | assert_equal 'bartext', r.bar
137 | assert_equal [:foo2,:foo3], r.read
138 |
139 | r.foo = 'foonew'
140 | r.foo2 = 'foo2new'
141 | r.foo3 = 'foo3new'
142 | r.bar = 'barnew'
143 | xml2 = r.save_to_xml
144 | assert_equal 'foonew', xml2.first_xpath("foo").text
145 | assert_equal 'foo2new', xml2.first_xpath("foo2").text
146 | assert_equal 'foo3new', xml2.first_xpath("foo3").text
147 | assert_equal 'barnew', xml2.first_xpath("bar").text
148 | end
149 |
150 |
151 | def test_writer
152 | xml = REXML::Document.new("
153 | footext
154 | foo2text
155 | foo3text
156 | bartext
157 | ").root
158 | w = WriterTest.load_from_xml xml
159 | assert_equal 'footext', w.foo
160 | assert_equal 'foo2text', w.foo2
161 | assert_equal 'foo3text', w.foo3
162 | assert_equal 'bartext', w.bar
163 |
164 | w.foo = 'foonew'
165 | w.foo2 = 'foo2new'
166 | w.foo3 = 'foo3new'
167 | w.bar = 'barnew'
168 | xml2 = w.save_to_xml
169 | assert_equal 'foonew', xml2.first_xpath("foo").text
170 | assert_nil xml2.first_xpath("foo2",:allow_nil=>true)
171 | assert_equal 'foo3new', xml2.first_xpath("foo3").text
172 | assert_equal 'barnew', xml2.first_xpath("bar").text
173 |
174 | assert_equal %w{dingdong2 dingdong3}, xml2.all_xpath("quux").map{|elt|elt.text}
175 | end
176 |
177 | def test_reader_writer_proc_vs_lambda
178 | xml = REXML::Document.new("
179 | proc_2args_text
180 | lambda_2args_text
181 | proc_3args_text
182 | lambda_3args_text
183 | ").root
184 | r = ReaderWriterProcVsLambdaTest.load_from_xml xml
185 | assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.read
186 | assert_nil r.written
187 | assert_nil r.proc_2args
188 | assert_nil r.lambda_2args
189 | assert_equal 'proc_3args_text', r.proc_3args
190 | assert_equal 'lambda_3args_text', r.lambda_3args
191 |
192 | r.proc_2args = "proc_2args_text_new"
193 | r.lambda_2args = "lambda_2args_text_new"
194 | r.proc_3args = "proc_3args_text_new"
195 | r.lambda_3args = "lambda_3args_text_new"
196 | xml2 = r.save_to_xml
197 | assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.written
198 | assert_nil xml2.first_xpath("proc_2args", :allow_nil=>true)
199 | assert_nil xml2.first_xpath("lambda_2args", :allow_nil=>true)
200 | assert_equal 'proc_3args_text_new', xml2.first_xpath("proc_3args").text
201 | assert_equal 'lambda_3args_text_new', xml2.first_xpath("lambda_3args").text
202 | end
203 |
204 | def test_setter_text_node
205 | @c.ent2 = "lalala"
206 | assert_equal "lalala", REXML::XPath.first(@c.save_to_xml, "arrtest/entry[2]").text
207 | end
208 |
209 |
210 | def test_setter_array_node
211 | xml=@c.save_to_xml
212 | assert_equal ["pencils", "weapons of mass destruction"],
213 | XML::XXPath.new("offices/office/@speciality").all(xml).map{|n|n.text}
214 | end
215 |
216 |
217 | def test_setter_hash_node
218 | xml=@c.save_to_xml
219 | assert_equal @c.customers.keys.sort,
220 | XML::XXPath.new("customers/customer/@uid").all(@xml.root).map{|n|n.text}.sort
221 | end
222 |
223 |
224 | def test_setter_boolean_node
225 | @c.offices[0].classified = !@c.offices[0].classified
226 | xml=@c.save_to_xml
227 | assert_equal @c.offices[0].classified,
228 | XML::XXPath.new("offices/office[1]/classified").first(xml).text == "yes"
229 | end
230 |
231 |
232 | def test_setter_choice_node
233 | xml=@c.save_to_xml
234 | thingselts = xml.all_xpath("stuff2/thing")
235 | assert_equal @c.things.size, thingselts.size
236 | assert_equal @c.things[0].name, thingselts[0].first_xpath("name").text
237 | assert_equal @c.things[1].name, thingselts[1].first_xpath("name").text
238 | assert_equal @c.things[2].name, thingselts[2].first_xpath("name").text
239 | assert_equal @c.things[3].name, thingselts[3].first_xpath("name").text
240 | end
241 |
242 |
243 | def test_setter_choice_node_multiple_attrs
244 | n1 = Names1.new
245 | assert_raises(XML::MappingError) {
246 | n1.save_to_xml # no choice present in n1
247 | }
248 |
249 | n1.names = ["multi1","multi2"]
250 | xml = n1.save_to_xml
251 | assert_equal n1.names, xml.all_xpath("names/name").map{|elt|elt.text}
252 | assert_nil xml.first_xpath("name", :allow_nil=>true)
253 |
254 | n1.name = "foo"
255 | xml = n1.save_to_xml
256 | assert_equal [], xml.all_xpath("names/name").map{|elt|elt.text}
257 | assert_equal n1.name, xml.first_xpath("name", :allow_nil=>true).text
258 | end
259 |
260 |
261 | def test_root_element
262 | assert_equal @c, XML::Mapping.load_object_from_file(File.dirname(__FILE__) + "/fixtures/company1.xml")
263 | assert_equal @c, XML::Mapping.load_object_from_xml(@xml.root)
264 |
265 | assert_equal "company", Company.root_element_name
266 | assert_equal Company, XML::Mapping.class_for_root_elt_name("company")
267 | xml=@c.save_to_xml
268 | assert_equal "company", xml.name
269 | # Company.root_element_name 'my-test'
270 | Company.class_eval <<-EOS
271 | root_element_name 'my-test'
272 | EOS
273 | assert_equal "my-test", Company.root_element_name
274 | assert_equal Company, XML::Mapping.class_for_root_elt_name("my-test")
275 | assert_nil XML::Mapping.class_for_root_elt_name("company")
276 | xml=@c.save_to_xml
277 | assert_equal "my-test", xml.name
278 | assert_equal "office", @c.offices[0].save_to_xml.name
279 |
280 | assert_raises(XML::MappingError) {
281 | XML::Mapping.load_object_from_xml @xml.root
282 | }
283 | @xml.root.name = 'my-test'
284 | assert_equal @c, XML::Mapping.load_object_from_xml(@xml.root)
285 |
286 | # white-box tests
287 | #assert_equal [["my-test", {:_default=>Company}]], XML::Mapping::Classes_w_nondefault_rootelt_names.sort
288 | #assert_equal [["address", {:_default=>Address}],
289 | # ["company", {}],
290 | # ["customer", {:_default=>Customer}],
291 | # ["office", {:_default=>Office}]],
292 | # XML::Mapping::Classes_w_default_rootelt_names.sort
293 | end
294 |
295 |
296 | def test_optional_flag
297 | hamburg_address_path = XML::XXPath.new("offices/office[1]/address")
298 | baghdad_address_path = XML::XXPath.new("offices/office[2]/address")
299 | hamburg_zip_path = XML::XXPath.new("offices/office[1]/address/zip")
300 | baghdad_zip_path = XML::XXPath.new("offices/office[2]/address/zip")
301 |
302 | assert_equal 18282, @c.offices[0].address.zip
303 | assert_equal 12576, @c.offices[1].address.zip
304 | xml=@c.save_to_xml
305 | assert_equal "18282", hamburg_zip_path.first(xml).text
306 | assert_nil baghdad_zip_path.first(xml,:allow_nil=>true)
307 | @c.offices[1].address.zip = 12577
308 | xml=@c.save_to_xml
309 | assert_equal "12577", baghdad_zip_path.first(xml).text
310 | c2 = Company.load_from_xml(xml)
311 | assert_equal 12577, c2.offices[1].address.zip
312 | @c.offices[1].address.zip = 12576
313 | xml=@c.save_to_xml
314 | assert_nil baghdad_zip_path.first(xml,:allow_nil=>true)
315 |
316 | hamburg_address_path.first(xml).delete_element("zip")
317 | c3 = Company.load_from_xml(xml)
318 | assert_equal 12576, c3.offices[0].address.zip
319 | hamburg_address_path.first(xml).delete_element("city")
320 | assert_raises(XML::MappingError) {
321 | Company.load_from_xml(xml)
322 | }
323 | end
324 |
325 |
326 | def test_optional_flag_nodefault
327 | hamburg_address_path = XML::XXPath.new("offices/office[1]/address")
328 | hamburg_street_path = XML::XXPath.new("offices/office[1]/address/street")
329 |
330 | assert_equal hamburg_street_path.first(@xml.root).text,
331 | @c.offices[0].address.street
332 |
333 | hamburg_address_path.first(@xml.root).delete_element("street")
334 | c2 = Company.load_from_xml(@xml.root)
335 | assert_nil c2.offices[0].address.street
336 |
337 | xml2=c2.save_to_xml
338 | assert_nil hamburg_street_path.first(xml2,:allow_nil=>true)
339 | end
340 |
341 |
342 | def test_default_value_identity_on_initialize
343 | c = Company.new
344 | assert_equal ["default"], c.test_default_value_identity
345 | c.test_default_value_identity << "foo"
346 |
347 | c2 = Company.new
348 | assert_equal ["default"], c2.test_default_value_identity
349 | end
350 |
351 |
352 | def test_default_value_identity_on_load
353 | assert_equal ["default"], @c.test_default_value_identity
354 | @c.test_default_value_identity << "bar"
355 |
356 | c2 = Company.load_from_file(File.dirname(__FILE__) + "/fixtures/company1.xml")
357 | assert_equal ["default"], c2.test_default_value_identity
358 | end
359 |
360 |
361 | def test_polymorphic_node
362 | assert_equal 3, @c.stuff.size
363 | assert_equal 'Saddam Hussein', @c.stuff[0].name
364 | assert_equal 'Berlin', @c.stuff[1].city
365 | assert_equal 'weapons of mass destruction', @c.stuff[2].speciality
366 |
367 | @c.stuff[1].city = 'Munich'
368 | @c.stuff[2].classified = false
369 |
370 | xml2=@c.save_to_xml
371 | assert_equal 'Munich', xml2.root.elements[5].elements[2].elements[1].text
372 | assert_equal 'no', xml2.root.elements[5].elements[3].elements[1].text
373 | end
374 |
375 |
376 | def test_file_io
377 | require 'tmpdir'
378 | Dir.mktmpdir do |dir|
379 | @c.save_to_file "#{dir}/out.xml"
380 | c2 = Company.load_from_file "#{dir}/out.xml"
381 | assert_equal @c, c2, 'read back object equals original'
382 |
383 | @c.save_to_file "#{dir}/out_default.xml", :formatter=>REXML::Formatters::Default.new
384 |
385 | assert FileUtils.compare_file("#{dir}/out.xml", "#{dir}/out_default.xml"), 'default formatter is Formatters::Default'
386 | assert File.open("#{dir}/out_default.xml").readlines.grep(/^\s/).empty?, 'default formatter produces no indentations'
387 |
388 | @c.save_to_file "#{dir}/out_pretty.xml", :formatter=>REXML::Formatters::Pretty.new
389 | assert not(File.open("#{dir}/out_pretty.xml").readlines.grep(/^\s/).empty?), 'pretty formatter does produce indentations'
390 |
391 | Company.class_eval <<-EOS
392 | mapping_output_formatter REXML::Formatters::Pretty.new
393 | EOS
394 |
395 | @c.save_to_file "#{dir}/out2.xml"
396 | assert FileUtils.compare_file("#{dir}/out2.xml", "#{dir}/out_pretty.xml"), 'default formatter can be changed on a per-class basis'
397 | end
398 | end
399 |
400 | end
401 |
--------------------------------------------------------------------------------
/test/xpath_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 |
5 | require "rexml/document"
6 | require "xml/xxpath"
7 | require "xml/xxpath_methods"
8 |
9 |
10 | class XPathTest < Test::Unit::TestCase
11 | include REXML
12 |
13 | def setup
14 | @d = Document.new <<-EOS
15 |
16 | x
17 | bar1
18 |
19 | y
20 |
21 |
22 | bar2
23 |
24 | bar3
25 |
26 |
27 | quux1
28 |
29 |
30 | bar4
31 |
32 |
33 | z
34 |
35 | bar5
36 |
37 |
38 |
39 |
40 |
41 | EOS
42 | end
43 |
44 | def test_read_byname
45 | assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo").all(@d.root)
46 | assert_equal @d.root.elements.to_a("foo")[1].elements.to_a("u"), XML::XXPath.new("foo/u").all(@d.root)
47 | assert_equal [], XML::XXPath.new("foo/notthere").all(@d.root)
48 | end
49 |
50 |
51 | def test_read_byidx
52 | assert_equal [@d.root.elements[1]], XML::XXPath.new("foo[1]").all(@d.root)
53 | assert_equal [@d.root.elements[3]], XML::XXPath.new("foo[2]").all(@d.root)
54 | assert_equal [], XML::XXPath.new("foo[10]").all(@d.root)
55 | assert_equal [], XML::XXPath.new("foo[3]").all(@d.root)
56 | end
57 |
58 |
59 | def test_read_byall
60 | assert_equal @d.root.elements.to_a, XML::XXPath.new("*").all(@d.root)
61 | assert_equal [], XML::XXPath.new("notthere/*").all(@d.root)
62 | end
63 |
64 |
65 | def test_read_bythisnode
66 | assert_equal [@d.root], XML::XXPath.new(".").all(@d.root)
67 | assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo/.").all(@d.root)
68 | assert_equal @d.root.elements.to_a("foo"), XML::XXPath.new("foo/./././.").all(@d.root)
69 | assert_equal @d.root.elements.to_a("foo")[0], XML::XXPath.new("foo/.").first(@d.root)
70 | assert_equal @d.root.elements.to_a("foo")[0], XML::XXPath.new("foo/./././.").first(@d.root)
71 | end
72 |
73 |
74 | def test_read_byattr
75 | assert_equal [@d.root.elements[3]], XML::XXPath.new("foo[@key='xy']").all(@d.root)
76 | assert_equal [], XML::XXPath.new("foo[@key='notthere']").all(@d.root)
77 | assert_equal [], XML::XXPath.new("notthere[@key='xy']").all(@d.root)
78 | end
79 |
80 |
81 | def test_attribute
82 | elt = @d.root.elements[3]
83 | attr1 = XML::XXPath::Accessors::Attribute.new(elt,"key",false)
84 | attr2 = XML::XXPath::Accessors::Attribute.new(elt,"key",false)
85 | assert_not_nil attr1
86 | assert_not_nil attr2
87 | assert_equal attr1,attr2 # tests Attribute.==
88 | assert_nil XML::XXPath::Accessors::Attribute.new(elt,"notthere",false)
89 | assert_nil XML::XXPath::Accessors::Attribute.new(elt,"notthere",false)
90 | newattr = XML::XXPath::Accessors::Attribute.new(elt,"new",true)
91 | assert_not_nil newattr
92 | assert_equal newattr, XML::XXPath::Accessors::Attribute.new(elt,"new",false)
93 | newattr.text = "lala"
94 | assert_equal "lala", elt.attributes["new"]
95 | end
96 |
97 | def test_read_byattrname
98 | assert_equal [XML::XXPath::Accessors::Attribute.new(@d.root.elements[3],"key",false)],
99 | XML::XXPath.new("foo/@key").all(@d.root)
100 | assert_equal [], XML::XXPath.new("foo/@notthere").all(@d.root)
101 | end
102 |
103 |
104 | def test_read_byidx_then_name
105 | assert_equal [@d.root.elements[3].elements[1]], XML::XXPath.new("foo[2]/u").all(@d.root)
106 | assert_equal [], XML::XXPath.new("foo[2]/notthere").all(@d.root)
107 | assert_equal [], XML::XXPath.new("notthere[2]/u").all(@d.root)
108 | assert_equal [], XML::XXPath.new("foo[3]/u").all(@d.root)
109 | end
110 |
111 |
112 | def test_read_alternative_names
113 | assert_equal ["bar3","quux1","bar4"],
114 | XML::XXPath.new("foo/bar/bar|quux").all(@d.root).map{|node|node.text.strip}
115 | end
116 |
117 |
118 | def test_read_attr
119 | assert_equal [@d.root.elements[3]],
120 | XML::XXPath.new(".[@key='xy']").all(@d.root.elements[3])
121 | assert_equal [@d.root.elements[3]],
122 | XML::XXPath.new("self::*[@key='xy']").all(@d.root.elements[3])
123 | assert_equal [],
124 | XML::XXPath.new(".[@key='xz']").all(@d.root.elements[3])
125 |
126 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/.[@key='xy']")
127 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/self::*[@key='xy']")
128 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/*/.[@key='xy']")
129 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/*/self::*[@key='xy']")
130 | assert_equal [], @d.all_xpath("bla/foo[2]/.[@key='xy2']")
131 | assert_equal [], @d.all_xpath("bla/foo[2]/.[@key2='xy']")
132 | assert_equal [], @d.all_xpath("bla/foo[2]/self::*[@key2='xy']")
133 | end
134 |
135 |
136 | def test_read_textnodes
137 | assert_equal ["bar3"], @d.root.all_xpath("foo[2]/bar/bar[1]/text()").map{|x|x.text.strip}
138 | end
139 |
140 |
141 | def test_read_descendant
142 | assert_equal ["bar1","bar2","bar3","bar4","bar5"],
143 | XML::XXPath.new("//bar").all(@d.root).map{|node|node.text.strip}
144 | assert_equal ["bar2","bar3"],
145 | XML::XXPath.new("//bar[@barkey='hello']").all(@d.root).map{|node|node.text.strip}
146 | assert_equal ["bar2","bar5"],
147 | XML::XXPath.new("//foo/bar").all(@d.root).map{|node|node.text.strip}
148 | assert_equal ["bar3","bar4"],
149 | XML::XXPath.new("//bar/bar").all(@d.root).map{|node|node.text.strip}
150 | assert_equal ["bar2","bar3","bar4","bar5"],
151 | XML::XXPath.new("foo//bar").all(@d.root).map{|node|node.text.strip}
152 | assert_equal ["bar2","bar3","bar4","bar5","bar5"],
153 | XML::XXPath.new("//foo//bar").all(@d.root).map{|node|node.text.strip}
154 | assert_equal ["z"],
155 | XML::XXPath.new("//bar//foo").all(@d.root).map{|node|node.text.strip}
156 | assert_equal ["bar2","bar3","quux1"],
157 | XML::XXPath.new("//@barkey").all(@d.root).map{|node|node.parent.text.strip}
158 | end
159 |
160 |
161 | def test_read_first
162 | assert_equal @d.root.elements[3].elements[1], XML::XXPath.new("foo[2]/u").first(@d.root)
163 | end
164 |
165 | def test_read_first_nil
166 | assert_equal nil, XML::XXPath.new("foo[2]/notthere").first(@d.root, :allow_nil=>true)
167 | end
168 |
169 | def test_read_first_exception
170 | assert_raises(XML::XXPathError) {
171 | XML::XXPath.new("foo[2]/notthere").first(@d.root)
172 | }
173 | end
174 |
175 |
176 | def test_write_noop
177 | assert_equal @d.root.elements[1], XML::XXPath.new("foo").first(@d.root, :ensure_created=>true)
178 | assert_equal @d.root.elements[3].elements[1], XML::XXPath.new("foo[2]/u").first(@d.root, :ensure_created=>true)
179 | # TODO: deep-compare of REXML documents?
180 | end
181 |
182 | def test_write_byname_then_name
183 | s1 = @d.elements[1].elements.size
184 | s2 = @d.elements[1].elements[1].elements.size
185 | node = XML::XXPath.new("foo/new1").first(@d.root, :ensure_created=>true)
186 | assert_equal "new1", node.name
187 | assert node.attributes.empty?
188 | assert_equal @d.elements[1].elements[1].elements[1], node
189 | assert_equal s1, @d.elements[1].elements.size
190 | assert_equal s2+1, @d.elements[1].elements[1].elements.size
191 | end
192 |
193 |
194 | def test_write_byidx
195 | XML::XXPath.new("foo[2]").first(@d.root, :ensure_created=>true)
196 | # TODO: deep-compare of REXML documents?
197 | assert_equal 2, @d.root.elements.select{|elt| elt.name=="foo"}.size
198 | node = XML::XXPath.new("foo[10]").first(@d.root, :ensure_created=>true)
199 | assert_equal 10, @d.root.elements.select{|elt| elt.name=="foo"}.size
200 | assert_equal "foo", node.name
201 | end
202 |
203 |
204 | def test_write_byattrname
205 | elt = @d.root.elements[3]
206 | s1 = elt.attributes.size
207 | attr_key = XML::XXPath.new("foo[2]/@key").first(@d.root, :ensure_created=>true)
208 | assert_equal elt.attributes["key"], attr_key.text
209 |
210 | attr_new = XML::XXPath.new("foo[2]/@new").first(@d.root, :ensure_created=>true)
211 | attr_new.text = "haha"
212 | assert_equal "haha", attr_new.text
213 | assert_equal "haha", elt.attributes["new"]
214 | assert_equal s1+1, elt.attributes.size
215 | end
216 |
217 |
218 | def test_write_byname_and_attr
219 | node1 = XML::XXPath.new("hiho[@blubb='bla']").first(@d.root,:ensure_created=>true)
220 | node2 = XML::XXPath.new("hiho[@blubb='bla']").first(@d.root,:ensure_created=>true)
221 | node3 = XML::XXPath.new("hiho[@blubb2='bla2']").first(@d.root,:ensure_created=>true)
222 | assert_equal node1, node2
223 | assert_equal node2, node3
224 | assert_equal "hiho", node1.name
225 | assert_equal 4, @d.root.elements.size
226 | assert_equal @d.root.elements[4], node1
227 | assert_equal @d.root.elements[4], node3
228 | assert_equal 'bla', node3.attributes['blubb']
229 | assert_equal 'bla2', node3.attributes['blubb2']
230 |
231 | node4 = XML::XXPath.new("hiho[@blubb='foo42']").first(@d.root,:ensure_created=>true)
232 | assert_not_equal node3, node4
233 | assert_equal 5, @d.root.elements.size
234 | assert_equal @d.root.elements[5], node4
235 | assert_equal 'foo42', node4.attributes['blubb']
236 | end
237 |
238 |
239 | def test_write_alternative_names
240 | node = XML::XXPath.new("foo/bar/bar|quux").first(@d.root,:ensure_created=>true)
241 | assert_equal XML::XXPath.new("foo/bar/bar").first(@d.root), node
242 |
243 | node = XML::XXPath.new("foo/bar/bar|quux").create_new(@d.root)
244 | assert node.unspecified?
245 | end
246 |
247 |
248 | def test_write_attr
249 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/.[@key='xy']", :ensure_created=>true)
250 | assert_equal "xy", @d.root.elements[3].attributes['key']
251 | assert_equal [@d.root.elements[3]], @d.all_xpath("bla/foo[2]/self::*[@key2='ab']", :ensure_created=>true)
252 | assert_equal "ab", @d.root.elements[3].attributes['key2']
253 | assert_equal "xy", @d.root.elements[3].attributes['key']
254 |
255 | assert_raises(XML::XXPathError) {
256 | @d.root.elements[3].create_new_xpath ".[@key='xy']"
257 | }
258 | assert_raises(XML::XXPathError) {
259 | @d.root.elements[3].create_new_xpath "self::*[@notthere='foobar']"
260 | }
261 | end
262 |
263 |
264 | def test_write_textnodes
265 | @d.root.create_new_xpath("morestuff/text()").text = "hello world"
266 | assert_equal "hello world", @d.root.first_xpath("morestuff").text
267 | end
268 |
269 |
270 | def test_write_descendant
271 | assert_equal @d.root.elements[3].elements[2].elements[2],
272 | node1 = XML::XXPath.new("//bar[@barkey='hello']//quux").first(@d.root,:ensure_created=>true)
273 | node1 = XML::XXPath.new("//bar[@barkey='hello']/hiho").first(@d.root,:ensure_created=>true)
274 | assert_equal "hiho", node1.name
275 | assert_equal @d.root.elements[3].elements[2], node1.parent
276 |
277 | node1 = XML::XXPath.new("/foo//quux/new").first(@d.root,:ensure_created=>true)
278 | assert_equal "new", node1.name
279 | assert_equal @d.root.elements[3].elements[2].elements[2], node1.parent
280 |
281 | assert_raises(XML::XXPathError) {
282 | XML::XXPath.new("//bar[@barkey='hello']//new2").first(@d.root,:ensure_created=>true)
283 | }
284 | end
285 |
286 |
287 | def test_write_bythisnode
288 | s1 = @d.elements[1].elements.size
289 | s2 = @d.elements[1].elements[1].elements.size
290 | node = XML::XXPath.new("foo/././.").first(@d.root, :ensure_created=>true)
291 | assert_equal @d.elements[1].elements[1], node
292 |
293 | node = XML::XXPath.new("foo/new1/././.").first(@d.root, :ensure_created=>true)
294 | assert_equal "new1", node.name
295 | assert node.attributes.empty?
296 | assert_equal @d.elements[1].elements[1].elements[1], node
297 | assert_equal s1, @d.elements[1].elements.size
298 | assert_equal s2+1, @d.elements[1].elements[1].elements.size
299 | end
300 |
301 |
302 | def test_create_new_byname
303 | s1 = @d.elements[1].elements.size
304 | s2 = @d.elements[1].elements[1].elements.size
305 | startnode = @d.elements[1].elements[1]
306 | node1 = XML::XXPath.new("new1").create_new(startnode)
307 | node2 = XML::XXPath.new("new1").first(startnode, :create_new=>true) #same as .create_new(...)
308 | assert_equal "new1", node1.name
309 | assert_equal "new1", node2.name
310 | assert node1.attributes.empty?
311 | assert node2.attributes.empty?
312 | assert_equal @d.elements[1].elements[1].elements[1], node1
313 | assert_equal @d.elements[1].elements[1].elements[2], node2
314 | assert_equal s1, @d.elements[1].elements.size
315 | assert_equal s2+2, @d.elements[1].elements[1].elements.size
316 | end
317 |
318 |
319 | def test_create_new_byname_then_name
320 | s1 = @d.elements[1].elements.size
321 | node1 = XML::XXPath.new("foo/new1").create_new(@d.root)
322 | node2 = XML::XXPath.new("foo/new1").create_new(@d.root)
323 | assert_equal "new1", node1.name
324 | assert_equal "new1", node2.name
325 | assert node1.attributes.empty?
326 | assert node2.attributes.empty?
327 | assert_equal @d.elements[1].elements[s1+1].elements[1], node1
328 | assert_equal @d.elements[1].elements[s1+2].elements[1], node2
329 | assert_equal s1+2, @d.elements[1].elements.size
330 | end
331 |
332 |
333 | def test_create_new_byidx
334 | assert_raises(XML::XXPathError) {
335 | XML::XXPath.new("foo[2]").create_new(@d.root)
336 | }
337 | node1 = XML::XXPath.new("foo[3]").create_new(@d.root)
338 | assert_raises(XML::XXPathError) {
339 | XML::XXPath.new("foo[3]").create_new(@d.root)
340 | }
341 | assert_equal @d.elements[1].elements[4], node1
342 | assert_equal "foo", node1.name
343 | node2 = XML::XXPath.new("foo[4]").create_new(@d.root)
344 | assert_equal @d.elements[1].elements[5], node2
345 | assert_equal "foo", node2.name
346 | node3 = XML::XXPath.new("foo[10]").create_new(@d.root)
347 | assert_raises(XML::XXPathError) {
348 | XML::XXPath.new("foo[10]").create_new(@d.root)
349 | }
350 | XML::XXPath.new("foo[11]").create_new(@d.root)
351 | assert_equal @d.elements[1].elements[11], node3
352 | assert_equal "foo", node3.name
353 | # @d.write
354 | end
355 |
356 | def test_create_new_byname_then_idx
357 | node1 = XML::XXPath.new("hello/bar[3]").create_new(@d.root)
358 | node2 = XML::XXPath.new("hello/bar[3]").create_new(@d.root)
359 | # same as create_new
360 | node3 = XML::XXPath.new("hello/bar[3]").create_new(@d.root)
361 | assert_equal @d.elements[1].elements[4].elements[3], node1
362 | assert_equal @d.elements[1].elements[5].elements[3], node2
363 | assert_equal @d.elements[1].elements[6].elements[3], node3
364 | assert_not_equal node1, node2
365 | assert_not_equal node1, node3
366 | assert_not_equal node2, node3
367 | end
368 |
369 |
370 | def test_create_new_byattrname
371 | node1 = XML::XXPath.new("@lala").create_new(@d.root)
372 | assert_raises(XML::XXPathError) {
373 | XML::XXPath.new("@lala").create_new(@d.root)
374 | }
375 | assert node1.kind_of?(XML::XXPath::Accessors::Attribute)
376 | node1.text = "val1"
377 | assert_equal "val1", @d.elements[1].attributes["lala"]
378 | foo2 = XML::XXPath.new("foo[2]").first(@d.root)
379 | assert_raises(XML::XXPathError) {
380 | XML::XXPath.new("@key").create_new(foo2)
381 | }
382 | node2 = XML::XXPath.new("@bar").create_new(foo2)
383 | assert node2.kind_of?(XML::XXPath::Accessors::Attribute)
384 | node2.text = "val2"
385 | assert_equal "val2", @d.elements[1].elements[3].attributes["bar"]
386 | end
387 |
388 |
389 | def test_create_new_byname_and_attr
390 | node1 = XML::XXPath.new("hiho[@blubb='bla']").create_new(@d.root)
391 | node2 = XML::XXPath.new("hiho[@blubb='bla']").create_new(@d.root)
392 | node3 = XML::XXPath.new("hiho[@blubb2='bla']").create_new(@d.root)
393 | assert_equal "hiho", node1.name
394 | assert_equal "hiho", node2.name
395 | assert_equal @d.root.elements[4], node1
396 | assert_equal @d.root.elements[5], node2
397 | assert_equal @d.root.elements[6], node3
398 | assert_not_equal @d.root.elements[5], node1
399 | end
400 |
401 |
402 | def test_create_new_bythisnode
403 | s1 = @d.elements[1].elements.size
404 | s2 = @d.elements[1].elements[1].elements.size
405 | startnode = @d.elements[1].elements[1]
406 | assert_raises(XML::XXPathError) {
407 | node1 = XML::XXPath.new("new1/.").create_new(startnode)
408 | }
409 | assert_raises(XML::XXPathError) {
410 | node2 = XML::XXPath.new("new1/././.").first(startnode, :create_new=>true)
411 | }
412 | end
413 |
414 |
415 | def test_unspecifiedness
416 | node1 = XML::XXPath.new("foo/hello").create_new(@d.root)
417 | assert(!(node1.unspecified?))
418 | assert_equal @d.root, node1.parent.parent
419 | node2 = XML::XXPath.new("foo/*").create_new(@d.root)
420 | assert_equal @d.root, node2.parent.parent
421 | assert node2.unspecified?
422 | node2.name = "newone"
423 | assert_equal "newone", node2.name
424 | assert(!(node2.unspecified?))
425 | end
426 |
427 | end
428 |
--------------------------------------------------------------------------------
/test/xxpath_benchmark.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/benchmark_fixtures"
2 |
3 | require "xml/xxpath"
4 |
5 | require 'benchmark'
6 | include Benchmark
7 |
8 |
9 | xxpath_by_name = XML::XXPath.new(@path_by_name)
10 | xxpath_by_idx = XML::XXPath.new(@path_by_idx) # "bar6"
11 | xxpath_by_idx_idx = XML::XXPath.new(@path_by_idx_idx) # "bar4-6"
12 | xxpath_by_attr_idx = XML::XXPath.new(@path_by_attr_idx) # "bar4-6"
13 | xxpath_by_attr = XML::XXPath.new(@path_by_attr) # "xy"
14 |
15 | rootelt = @d.root
16 | foo2elt = rootelt.elements[3]
17 | res1=res2=res3=res4=res5=nil
18 | print "(#{@count} runs)\n"
19 | bmbm(12) do |x|
20 | x.report("by_name") { @count.times { res1 = xxpath_by_name.first(rootelt) } }
21 | x.report("by_idx") { @count.times { res2 = xxpath_by_idx.first(rootelt) } }
22 | x.report("by_idx_idx") { @count.times { res3 = xxpath_by_idx_idx.first(rootelt) } }
23 | x.report("by_attr_idx") { @count.times { res4 = xxpath_by_attr_idx.first(rootelt) } }
24 | x.report("xxpath_by_attr") { (@count*4).times { res5 = xxpath_by_attr.first(foo2elt) } }
25 | end
26 |
27 |
28 | def assert_equal(expected,actual)
29 | expected==actual or raise "expected: #{expected.inspect}, actual: #{actual.inspect}"
30 | end
31 |
32 | assert_equal "bar4-2", res1.text.strip
33 | assert_equal "bar6", res2.text.strip
34 | assert_equal "bar4-6", res3.text.strip
35 | assert_equal "bar4-6", res4.text.strip
36 | assert_equal "xy", res5.text.strip
37 |
--------------------------------------------------------------------------------
/test/xxpath_methods_test.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__)+"/tests_init"
2 |
3 | require 'test/unit'
4 |
5 | require "rexml/document"
6 | require "xml/xxpath_methods"
7 |
8 |
9 | class XXPathMethodsTest < Test::Unit::TestCase
10 | include REXML
11 |
12 | def setup
13 | @d = Document.new <<-EOS
14 |
15 | x
16 | bar1
17 |
18 | y
19 |
20 |
21 |
22 | EOS
23 | end
24 |
25 | def test_first_xpath
26 | pathstr = "foo[2]/u"
27 | path = XML::XXPath.new(pathstr)
28 | elt = path.first(@d.root)
29 | assert_equal elt, @d.root.first_xpath(pathstr)
30 | assert_equal elt, @d.root.first_xpath(path)
31 | end
32 |
33 | def test_all_xpath
34 | pathstr = "foo"
35 | path = XML::XXPath.new(pathstr)
36 | elts = path.all(@d.root)
37 | assert_equal elts, @d.root.all_xpath(pathstr)
38 | assert_equal elts, @d.root.all_xpath(path)
39 | end
40 |
41 | def test_each_xpath
42 | pathstr = "foo"
43 | path = XML::XXPath.new(pathstr)
44 | elts = []
45 | path.each(@d.root) do |elt|
46 | elts << elt
47 | end
48 | elts_actual = []
49 | @d.root.each_xpath(pathstr) do |elt|
50 | elts_actual << elt
51 | end
52 | assert_equal elts, elts_actual
53 | end
54 |
55 | def test_create_new
56 | @d.root.create_new_xpath("foo")
57 | @d.root.create_new_xpath(XML::XXPath.new("foo"))
58 | assert_equal 4, @d.root.elements.to_a("foo").size
59 | end
60 |
61 | end
62 |
--------------------------------------------------------------------------------
/user_manual_xxpath.in.md:
--------------------------------------------------------------------------------
1 | # XML-XXPATH
2 |
3 | ## Overview, Motivation
4 |
5 | Xml-xxpath is an (incomplete) XPath interpreter that is at the moment
6 | bundled with xml-mapping. It is built on top of REXML. xml-mapping
7 | uses xml-xxpath extensively for implementing its node types -- see the
8 | README file and the reference documentation (and the source code) for
9 | details. xml-xxpath, however, does not depend on xml-mapping at all,
10 | and is useful in its own right -- maybe I'll later distribute it as a
11 | seperate library instead of bundling it. For the time being, if you
12 | want to use this XPath implementation stand-alone, you can just rip
13 | the files `lib/xml/xxpath.rb`, `lib/xml/xxpath/steps.rb`, and
14 | `lib/xml/xxpath_methods.rb` out of the xml-mapping distribution and
15 | use them on their own (they do not depend on anything else).
16 |
17 | xml-xxpath's XPath support is vastly incomplete (see below), but, in
18 | addition to the normal reading/matching functionality found in other
19 | XPath implementations (i.e. "find all elements in a given XML document
20 | matching a given XPath expression"), xml-xxpath supports write
21 | access. For example, when writing the XPath expression
22 | `/foo/bar[3]/baz[@key='hiho']` to the XML document
23 |
24 |
25 |
26 | hello
27 | goodbye
28 |
29 |
30 |
31 | , you'll get:
32 |
33 |
34 |
35 | hello
36 | goodbye
37 |
38 |
39 |
40 |
41 |
42 | This feature is used by xml-mapping when writing (marshalling) Ruby
43 | objects to XML, and is actually the reason why I couldn't just use any
44 | of the existing XPath implementations, e.g. the one that comes with
45 | REXML. Also, the whole xml-xxpath implementation is just 300 lines of
46 | Ruby code, it is quite fast (paths are precompiled), and xml-xxpath
47 | returns matched elements in the order they appeared in the source
48 | document -- I've heard REXML::XPath doesn't do that :)
49 |
50 | Some basic knowledge of XPath is helpful for reading this document.
51 |
52 | At the moment, xml-xxpath understands XPath expressions of the form
53 | [`/`]_pathelement_`/[/]`_pathelement_`/[/]`...,
54 | where each _pathelement_ must be one of these:
55 |
56 | - a simple element name _name_, e.g. `signature`
57 |
58 | - an attribute name, @_attrname_, e.g. `@key`
59 |
60 | - a combination of an element name and an attribute name and
61 | -value, in the form `elt_name[@attr_name='attr_value']`
62 |
63 | - an element name and an index, `elt_name[index]`
64 |
65 | - the "match-all" path element, `*`
66 |
67 | - .
68 |
69 | - name1`|`name2`|`...
70 |
71 | - `.[@key='xy'] / self::*[@key='xy']`
72 |
73 | - `child::*[@key='xy']`
74 |
75 | - `text()`
76 |
77 |
78 |
79 | Xml-xxpath only supports relative paths at this time, i.e. XPath
80 | expressions beginning with "/" or "//" will still only find nodes
81 | below the node the expression is applied to (as if you had written
82 | "./" or ".//", respectively).
83 |
84 |
85 | ## Usage
86 |
87 | Xml-xxpath defines the class XML::XXPath. An instance of that class
88 | wraps an XPath expression, the string representation of which must be
89 | supplied when constructing the instance. You then call instance
90 | methods like _first_, _all_ or create_new on the instance,
91 | supplying the REXML Element the XPath expression should be applied to,
92 | and get the results, or, in the case of write access, the element is
93 | updated in-place.
94 |
95 |
96 | ### Read Access
97 |
98 | :include: xpath_usage.intout
99 |
100 | The objects supplied to the `all()`, `first()`, and
101 | `each()` calls must be REXML element nodes, i.e. they must
102 | support messages like `elements`, `attributes` etc
103 | (instances of REXML::Element and its subclasses do this). The calls
104 | return the found elements as instances of REXML::Element or
105 | XML::XXPath::Accessors::Attribute. The latter is a wrapper around
106 | attribute nodes that is largely call-compatible to
107 | REXML::Element. This is so you can write things like
108 | `path.each{|node|puts node.text}` without having to
109 | special-case anything even if the path matches attributes, not just
110 | elements.
111 |
112 | As you can see, you can re-use path objects, applying them to
113 | different XML elements at will. You should do this because the XPath
114 | pattern is stored inside the XPath object in a pre-compiled form,
115 | which makes it more efficient.
116 |
117 | The path elements of the XPath pattern are applied to the
118 | `.elements` collection of the passed XML element and its
119 | sub-elements, starting with the first one. This is shown by the
120 | following code:
121 |
122 | :include: xpath_docvsroot.intout
123 |
124 | A REXML +Document+ object is a REXML +Element+ object whose +elements+
125 | collection consists only of a single member -- the document's root
126 | node. The first path element of the XPath -- "foo" in the example --
127 | is matched against that. That is why the path "/bar" in the example
128 | doesn't match anything when matched against the document +d+ itself.
129 |
130 | An ordinary REXML +Element+ object that represents a node somewhere
131 | inside an XML tree has an +elements+ collection that consists of all
132 | the element's direct sub-elements. That is why XPath patterns matched
133 | against the +firstelt+ element in the example *must not* start with
134 | "/first" (unless there is a child node that is also named "first").
135 |
136 |
137 | ### Write Access
138 |
139 | You may pass an `:ensure_created=>true` option argument to
140 | _path_.first(_elt_) / _path_.all(_elt_) calls to make sure that _path_
141 | exists inside the passed XML element _elt_. If it existed before,
142 | nothing changes, and the call behaves just as it would without the
143 | option argument. If the path didn't exist before, the XML element is
144 | modified such that
145 |
146 | - the path exists afterwards
147 |
148 | - all paths that existed before still exist afterwards
149 |
150 | - the modification is as small as possible (i.e. as few elements as
151 | possible are added, additional attributes are added to existing
152 | elements if possible etc.)
153 |
154 | The created resp. previously existing, matching elements are returned.
155 |
156 |
157 | Examples:
158 |
159 | :include: xpath_ensure_created.intout
160 |
161 |
162 | Alternatively, you may pass a `:create_new=>true` option
163 | argument or call `create_new` (_path_`.create_new(`_elt_`)` is
164 | equivalent to _path_`.first(`_elt_`,:create_new=>true)`). In that
165 | case, a new node is created in _elt_ for each path element of _path_
166 | (or an exception raised if that wasn't possible for any path element).
167 |
168 | Examples:
169 |
170 | :include: xpath_create_new.intout
171 |
172 | This feature is used in xml-mapping by node types like
173 | XML::Mapping::ArrayNode, which must create a new instance of the
174 | "per-array element path" for each element of the array to be stored in
175 | an XML tree.
176 |
177 |
178 | ### Pathological Cases
179 |
180 | What is created when the Path "*" is to be created inside an empty XML
181 | element? The name of the element to be created isn't known, but still
182 | some element must be created. The answer is that xml-xxpath creates a
183 | special "unspecified" element whose name must be set by the caller
184 | afterwards:
185 |
186 | :include: xpath_pathological.intout
187 |
188 | The "newelt" object in the last example is an ordinary
189 | REXML::Element. xml-xxpath mixes the "unspecified" attribute into that
190 | class, as well as into the XML::XXPath::Accessors::Attribute class
191 | mentioned above.
192 |
193 |
194 | ## Implentation notes
195 |
196 | `doc/xpath_impl_notes.txt` contains some documentation on the
197 | implementation of xml-xxpath.
198 |
199 | ## License
200 |
201 | Ruby's.
202 |
--------------------------------------------------------------------------------