├── .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 | --------------------------------------------------------------------------------