xml-mapping 0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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) #<=
|