xml-mapping 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +56 -0
- data/README +386 -0
- data/README_XPATH +175 -0
- data/Rakefile +214 -0
- data/TODO.txt +32 -0
- data/doc/xpath_impl_notes.txt +119 -0
- data/examples/company.rb +34 -0
- data/examples/company.xml +26 -0
- data/examples/company_usage.intin.rb +19 -0
- data/examples/company_usage.intout +39 -0
- data/examples/order.rb +61 -0
- data/examples/order.xml +54 -0
- data/examples/order_signature_enhanced.rb +7 -0
- data/examples/order_signature_enhanced.xml +9 -0
- data/examples/order_signature_enhanced_usage.intin.rb +12 -0
- data/examples/order_signature_enhanced_usage.intout +16 -0
- data/examples/order_usage.intin.rb +73 -0
- data/examples/order_usage.intout +147 -0
- data/examples/time_augm.intin.rb +19 -0
- data/examples/time_augm.intout +23 -0
- data/examples/time_node.rb +27 -0
- data/examples/xpath_create_new.intin.rb +85 -0
- data/examples/xpath_create_new.intout +181 -0
- data/examples/xpath_docvsroot.intin.rb +30 -0
- data/examples/xpath_docvsroot.intout +34 -0
- data/examples/xpath_ensure_created.intin.rb +62 -0
- data/examples/xpath_ensure_created.intout +114 -0
- data/examples/xpath_pathological.intin.rb +42 -0
- data/examples/xpath_pathological.intout +56 -0
- data/examples/xpath_usage.intin.rb +51 -0
- data/examples/xpath_usage.intout +57 -0
- data/install.rb +40 -0
- data/lib/xml/mapping.rb +14 -0
- data/lib/xml/mapping/base.rb +563 -0
- data/lib/xml/mapping/standard_nodes.rb +343 -0
- data/lib/xml/mapping/version.rb +8 -0
- data/lib/xml/xxpath.rb +354 -0
- data/test/all_tests.rb +6 -0
- data/test/company.rb +54 -0
- data/test/documents_folders.rb +33 -0
- data/test/fixtures/bookmarks1.xml +24 -0
- data/test/fixtures/company1.xml +85 -0
- data/test/fixtures/documents_folders.xml +71 -0
- data/test/fixtures/documents_folders2.xml +30 -0
- data/test/multiple_mappings.rb +80 -0
- data/test/tests_init.rb +2 -0
- data/test/xml_mapping_adv_test.rb +84 -0
- data/test/xml_mapping_test.rb +182 -0
- data/test/xpath_test.rb +273 -0
- metadata +96 -0
data/Rakefile
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# adapted from active_record's Rakefile
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rake'
|
6
|
+
require 'rake/testtask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/packagetask'
|
9
|
+
require 'rake/gempackagetask'
|
10
|
+
#require 'rake/contrib/rubyforgepublisher'
|
11
|
+
require 'rake/contrib/sshpublisher'
|
12
|
+
|
13
|
+
require File.dirname(__FILE__)+"/lib/xml/mapping/version"
|
14
|
+
|
15
|
+
|
16
|
+
# yeah -- it's just stupid that these are private
|
17
|
+
|
18
|
+
class Rake::RDocTask
|
19
|
+
public :rdoc_target
|
20
|
+
end
|
21
|
+
|
22
|
+
class Rake::PackageTask
|
23
|
+
public :tgz_file, :zip_file
|
24
|
+
end
|
25
|
+
|
26
|
+
class Rake::GemPackageTask
|
27
|
+
public :gem_file
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
FILES_RDOC_EXTRA=%w{README README_XPATH TODO.txt doc/xpath_impl_notes.txt}
|
32
|
+
FILES_RDOC_INCLUDES=%w{examples/company.xml
|
33
|
+
examples/company.rb
|
34
|
+
examples/company_usage.intout
|
35
|
+
examples/order_usage.intout
|
36
|
+
examples/time_augm.intout
|
37
|
+
examples/xpath_usage.intout
|
38
|
+
examples/xpath_ensure_created.intout
|
39
|
+
examples/xpath_create_new.intout
|
40
|
+
examples/xpath_pathological.intout
|
41
|
+
examples/xpath_docvsroot.intout
|
42
|
+
examples/order_signature_enhanced_usage.intout}
|
43
|
+
|
44
|
+
|
45
|
+
desc "Default Task"
|
46
|
+
task :default => [ :test ]
|
47
|
+
|
48
|
+
Rake::TestTask.new(:test) { |t|
|
49
|
+
t.test_files = ["test/all_tests.rb"]
|
50
|
+
t.verbose = true
|
51
|
+
}
|
52
|
+
|
53
|
+
# runs tests only if sources have changed since last succesful run of
|
54
|
+
# tests
|
55
|
+
file "test_run" => FileList.new('lib/**/*.rb','test/**/*.rb') do
|
56
|
+
Task[:test].invoke
|
57
|
+
touch "test_run"
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
Rake::RDocTask.new { |rdoc|
|
63
|
+
rdoc.rdoc_dir = 'doc/api'
|
64
|
+
rdoc.title = "XML::Mapping -- Simple, extensible Ruby-to-XML (and back) mapper"
|
65
|
+
rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object --include examples'
|
66
|
+
rdoc.rdoc_files.include(*FILES_RDOC_EXTRA)
|
67
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
68
|
+
|
69
|
+
# additional file dependencies for the rdoc task
|
70
|
+
# this somewhat of a black art because RDocTask doesn't document the
|
71
|
+
# prerequisite of its rdoc task (<rdoc_dir>/index.html)
|
72
|
+
file rdoc.rdoc_target => FILES_RDOC_INCLUDES
|
73
|
+
file "#{rdoc.rdoc_dir}/index.html" => FileList.new("examples/**/*.rb")
|
74
|
+
}
|
75
|
+
|
76
|
+
#rule '.intout' => ['.intin.rb', *FileList.new("lib/**/*.rb")] do |task| # doesn't work -- see below
|
77
|
+
rule '.intout' => ['.intin.rb'] do |task|
|
78
|
+
this_file_re = Regexp.compile(Regexp.quote(__FILE__))
|
79
|
+
b = binding
|
80
|
+
visible=true; visible_retval=true; handle_exceptions=false
|
81
|
+
old_stdout = $stdout
|
82
|
+
old_wd = Dir.pwd
|
83
|
+
begin
|
84
|
+
File.open(task.name,"w") do |fout|
|
85
|
+
$stdout = fout
|
86
|
+
File.open(task.source,"r") do |fin|
|
87
|
+
Dir.chdir File.dirname(task.name)
|
88
|
+
fin.read.split("#<=\n").each do |snippet|
|
89
|
+
|
90
|
+
snippet.scan(/^#:(.*?):$/) do |(switch,)|
|
91
|
+
case switch
|
92
|
+
when "visible"
|
93
|
+
visible=true
|
94
|
+
when "invisible"
|
95
|
+
visible=false
|
96
|
+
when "visible_retval"
|
97
|
+
visible_retval=true
|
98
|
+
when "invisible_retval"
|
99
|
+
visible_retval=false
|
100
|
+
when "handle_exceptions"
|
101
|
+
handle_exceptions=true
|
102
|
+
when "no_exceptions"
|
103
|
+
handle_exceptions=false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
snippet.gsub!(/^#:.*?:(?:\n|\z)/,'')
|
107
|
+
|
108
|
+
print "#{snippet}\n" if visible
|
109
|
+
exc_handled = false
|
110
|
+
value = begin
|
111
|
+
eval(snippet,b)
|
112
|
+
rescue Exception
|
113
|
+
raise unless handle_exceptions
|
114
|
+
exc_handled = true
|
115
|
+
if visible
|
116
|
+
print "#{$!.class}: #{$!}\n"
|
117
|
+
for m in $@
|
118
|
+
break if m=~this_file_re
|
119
|
+
print "\tfrom #{m}\n"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
if visible and visible_retval and not exc_handled
|
124
|
+
print "=> #{value.inspect}\n"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
rescue Exception
|
130
|
+
$stdout = old_stdout
|
131
|
+
Dir.chdir old_wd
|
132
|
+
File.delete task.name
|
133
|
+
raise
|
134
|
+
ensure
|
135
|
+
$stdout = old_stdout
|
136
|
+
Dir.chdir old_wd
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# have to add additional prerequisites manually because it appears
|
141
|
+
# that rules can only define a single prerequisite :-\
|
142
|
+
for f in %w{examples/company_usage
|
143
|
+
examples/order_usage
|
144
|
+
examples/order_signature_enhanced_usage
|
145
|
+
examples/time_augm
|
146
|
+
examples/xpath_usage
|
147
|
+
examples/xpath_ensure_created
|
148
|
+
examples/xpath_create_new
|
149
|
+
examples/xpath_pathological
|
150
|
+
examples/xpath_docvsroot} do
|
151
|
+
file "#{f}.intout" => ["#{f}.intin.rb", 'examples/company.xml']
|
152
|
+
file "#{f}.intout" => FileList.new("lib/**/*.rb")
|
153
|
+
file "#{f}.intout" => FileList.new("examples/**/*.rb")
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
spec = Gem::Specification.new do |s|
|
158
|
+
s.name = 'xml-mapping'
|
159
|
+
s.version = XML::Mapping::VERSION
|
160
|
+
s.platform = Gem::Platform::RUBY
|
161
|
+
s.summary =
|
162
|
+
"An easy to use, extensible library for mapping Ruby objects to XML and back. Includes an XPath interpreter."
|
163
|
+
|
164
|
+
# Rubygems' RDoc support is incomplete... Can't seem to find a way
|
165
|
+
# to set the start page, or a set of files that should be includable
|
166
|
+
# but not processed by rdoc directly
|
167
|
+
s.files += FILES_RDOC_EXTRA
|
168
|
+
s.files += Dir.glob("{lib,examples,test}/**/*").delete_if do |item|
|
169
|
+
item.include?("CVS") || item =~ /~$/
|
170
|
+
end
|
171
|
+
s.files += %w{LICENSE Rakefile install.rb}
|
172
|
+
s.extra_rdoc_files = FILES_RDOC_EXTRA
|
173
|
+
s.rdoc_options += %w{--include examples}
|
174
|
+
|
175
|
+
s.require_path = 'lib'
|
176
|
+
s.autorequire = 'xml/mapping'
|
177
|
+
|
178
|
+
# s.add_dependency 'rexml'
|
179
|
+
|
180
|
+
s.has_rdoc=true
|
181
|
+
|
182
|
+
s.test_file = 'test/all_tests.rb'
|
183
|
+
|
184
|
+
s.author = 'Olaf Klischat'
|
185
|
+
s.email = 'klischat@cs.tu-berlin.de'
|
186
|
+
s.homepage = "http://xml-mapping.rubyforge.org"
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
Rake::GemPackageTask.new(spec) do |p|
|
192
|
+
p.gem_spec = spec
|
193
|
+
p.need_tar = true
|
194
|
+
p.need_zip = true
|
195
|
+
|
196
|
+
# (indirectly) add :rdoc, :test as prerequisites to :package task
|
197
|
+
# created by GemPackageTask
|
198
|
+
file "#{p.package_dir}/#{p.tgz_file}" => [ "test_run", :rdoc ]
|
199
|
+
file "#{p.package_dir}/#{p.zip_file}" => [ "test_run", :rdoc ]
|
200
|
+
file "#{p.package_dir}/#{p.gem_file}" => [ "test_run", :rdoc ]
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
|
205
|
+
# run "rake package" to generate tgz, zip, gem in pkg/
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
task :rfpub_rdoc => [:rdoc] do
|
210
|
+
p=Rake::SshDirPublisher.new('xml-mapping.rubyforge.org',
|
211
|
+
'/var/www/gforge-projects/xml-mapping/',
|
212
|
+
'doc/api')
|
213
|
+
p.upload
|
214
|
+
end
|
data/TODO.txt
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
- XML::XXPath: generalize foo[@x='bar'] to foo[<any XPath
|
2
|
+
expression>='bar'] (unless create/create_new implementation proves
|
3
|
+
to be too difficult, but I don't think it will...)
|
4
|
+
|
5
|
+
- documentation:
|
6
|
+
|
7
|
+
- README:
|
8
|
+
|
9
|
+
- multi-attribute nodes
|
10
|
+
|
11
|
+
- XML::Mapping: Move @options hash functionality from
|
12
|
+
SingleAttributeNode to Node.
|
13
|
+
|
14
|
+
- XML::Mapping/default attribute values: Update documentation, digest
|
15
|
+
"nil" issues...
|
16
|
+
|
17
|
+
- add streaming input/output to XML::Mapping, i.e. SAX-based input in
|
18
|
+
addition to the current REXML/DOM - based one. Probably won't be
|
19
|
+
implementable for some more complicated XPaths -- raise meaningful
|
20
|
+
exceptions in those cases.
|
21
|
+
|
22
|
+
- XML::XXPath/XML::Mapping: add XML text nodes (the sub-node of an
|
23
|
+
element node that contains that element's text) first-class to
|
24
|
+
XML::XXPath. Use it for things like text_node :contents, "text()".
|
25
|
+
|
26
|
+
Along those lines: promote XPath node "unspecifiedness" from an
|
27
|
+
attribute to a REXML node object of "unspecified" class that's
|
28
|
+
turned into an attribute/element/text node when necessary
|
29
|
+
|
30
|
+
- (eventually, maybe) provide a "scaffolding" feature to automatically
|
31
|
+
turn a dtd/schema into a set of node type definitions or even a set
|
32
|
+
of mapping classes
|
@@ -0,0 +1,119 @@
|
|
1
|
+
=== latest design (12/2004)
|
2
|
+
|
3
|
+
At the lowest level, the "Accessors" sub-module contains reader and
|
4
|
+
creator functions that correspond to the various types of path
|
5
|
+
elements (_elt_name_, @_attr_name_,
|
6
|
+
_elt_name_[@_attr_name_='_attr_value_'] etc.) that xml-xxpath
|
7
|
+
supports. A reader function gets an array of nodes and the search
|
8
|
+
parameters corresponding to its path element type (e.g. _elt_name_,
|
9
|
+
_attr_name_, _attr_value_) and returns an array with all matching
|
10
|
+
direct sub-nodes of any of the supplied nodes. A creator function gets
|
11
|
+
one node and the search parameters and returns the created sub-node.
|
12
|
+
|
13
|
+
An XPath expression <tt><things1>/<things2>/.../<thingsx></tt> 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
|
+
- <tt>@creator_procs</tt> -- an array of "creator"
|
19
|
+
functions. <tt>@creator_procs[i]</tt> gets passed a base node (XML
|
20
|
+
element) and a create_new flag, and it creates the path
|
21
|
+
<tt><things[x-i+1]>/<things[x-i+2]>/.../<thingsx></tt> inside the
|
22
|
+
base node and returns the hindmost element created (i.e. the one
|
23
|
+
corresponding to <tt><thingsx></tt>).
|
24
|
+
|
25
|
+
- <tt>@reader_proc</tt> -- 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 <tt>@creator_procs</tt> 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+, <tt>create_new</tt> etc. are even more trivial
|
51
|
+
frontends to that.
|
52
|
+
|
53
|
+
The implementations of the <tt>@creator_procs</tt> 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_<thingsx>(node,create_new,<thingsx>),
|
61
|
+
create_new)
|
62
|
+
}
|
63
|
+
|
64
|
+
@creator_procs[2] =
|
65
|
+
proc {|node,create_new|
|
66
|
+
@creator_procs[1].call(Accessors.create_subnode_by_<thingsx-1>(node,create_new,<thingsx-1>),
|
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_<things[x+1-n]>(node,create_new,<things[x+1-n]>),
|
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_<things1>(node,create_new,<things1>),
|
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_<thingsx>(nodes,<thingsx>)
|
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_<thingsx-1>(nodes,<thingsx-1>)
|
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_<things1>(nodes,<things1>)
|
114
|
+
if (next_nodes == [])
|
115
|
+
throw :not_found, [nodes,@creator_procs[x]]
|
116
|
+
else
|
117
|
+
rpx-1.call(next_nodes)
|
118
|
+
end
|
119
|
+
}
|
data/examples/company.rb
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<?xml version="1.0" encoding="ISO-8859-1"?>
|
2
|
+
|
3
|
+
<company name="ACME inc.">
|
4
|
+
|
5
|
+
<address>
|
6
|
+
<city>Berlin</city>
|
7
|
+
<zip>10113</zip>
|
8
|
+
</address>
|
9
|
+
|
10
|
+
<customers>
|
11
|
+
|
12
|
+
<customer id="jim">
|
13
|
+
<name>James Kirk</name>
|
14
|
+
</customer>
|
15
|
+
|
16
|
+
<customer id="ernie">
|
17
|
+
<name>Ernie</name>
|
18
|
+
</customer>
|
19
|
+
|
20
|
+
<customer id="bert">
|
21
|
+
<name>Bert</name>
|
22
|
+
</customer>
|
23
|
+
|
24
|
+
</customers>
|
25
|
+
|
26
|
+
</company>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#:invisible:
|
2
|
+
$:.unshift "../lib"
|
3
|
+
begin
|
4
|
+
Object.send(:remove_const, "Address") # name clash with order_usage...
|
5
|
+
rescue
|
6
|
+
end
|
7
|
+
require 'company' #<=
|
8
|
+
#:visible:
|
9
|
+
c = Company.load_from_file('company.xml') #<=
|
10
|
+
c.name #<=
|
11
|
+
c.customers.size #<=
|
12
|
+
c.customers[1] #<=
|
13
|
+
c.customers[1].name #<=
|
14
|
+
c.customers[0].name #<=
|
15
|
+
c.customers[0].name = 'James Tiberius Kirk' #<=
|
16
|
+
c.customers << Customer.new('cm','Cookie Monster') #<=
|
17
|
+
xml2 = c.save_to_xml #<=
|
18
|
+
#:invisible_retval:
|
19
|
+
xml2.write($stdout,2) #<=
|