xml-mapping 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +10 -53
- data/README.md +57 -0
- data/Rakefile +75 -85
- data/TODO.txt +12 -2
- data/examples/README +4 -4
- data/examples/cleanup.rb +11 -0
- data/examples/company_usage.intout +7 -7
- data/examples/documents_folders_usage.intout +2 -2
- data/examples/order_signature_enhanced_usage.intout +3 -3
- data/examples/order_usage.intout +26 -26
- data/examples/person.intout +3 -3
- data/examples/person_mm.intout +2 -2
- data/examples/publication.intout +2 -2
- data/examples/reader.intout +1 -1
- data/examples/stringarray_usage.intout +1 -1
- data/examples/time_augm.intout +6 -6
- data/examples/xpath_create_new.intout +13 -13
- data/examples/xpath_ensure_created.intout +2 -2
- data/examples/xpath_usage.intout +1 -1
- data/lib/xml/mapping.rb +0 -2
- data/lib/xml/mapping/base.rb +24 -9
- data/lib/xml/mapping/version.rb +1 -1
- data/test/company.rb +43 -0
- data/test/documents_folders.rb +7 -0
- data/test/multiple_mappings_test.rb +3 -1
- data/test/xml_mapping_adv_test.rb +20 -20
- data/test/xml_mapping_test.rb +31 -2
- data/{README → user_manual.md} +916 -154
- data/user_manual_xxpath.md +677 -0
- metadata +100 -112
- data/ChangeLog +0 -189
- data/README_XPATH +0 -202
- data/examples/company_usage.intin.rb +0 -19
- data/examples/documents_folders_usage.intin.rb +0 -18
- data/examples/order_signature_enhanced_usage.intin.rb +0 -12
- data/examples/order_usage.intin.rb +0 -120
- data/examples/person.intin.rb +0 -44
- data/examples/person_mm.intin.rb +0 -119
- data/examples/publication.intin.rb +0 -44
- data/examples/reader.intin.rb +0 -33
- data/examples/stringarray_usage.intin.rb +0 -11
- data/examples/time_augm.intin.rb +0 -19
- data/examples/time_augm_loading.intin.rb +0 -44
- data/examples/time_node.intin.rb +0 -79
- data/examples/time_node_w_marshallers.intin.rb +0 -48
- data/examples/xpath_create_new.intin.rb +0 -85
- data/examples/xpath_docvsroot.intin.rb +0 -30
- data/examples/xpath_ensure_created.intin.rb +0 -62
- data/examples/xpath_pathological.intin.rb +0 -42
- data/examples/xpath_usage.intin.rb +0 -51
- data/install.rb +0 -41
- data/test/xxpath_benchmark.result1.txt +0 -17
@@ -15,7 +15,9 @@ class MultipleMappingsTest < Test::Unit::TestCase
|
|
15
15
|
Classes_by_rootelt_names.clear
|
16
16
|
EOS
|
17
17
|
Object.send(:remove_const, "Triangle")
|
18
|
-
$".delete "triangle_mm.rb"
|
18
|
+
unless ($".delete "triangle_mm.rb")
|
19
|
+
$".delete_if{|name| name =~ %r!test/triangle_mm.rb$!}
|
20
|
+
end
|
19
21
|
$:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb)
|
20
22
|
require 'triangle_mm'
|
21
23
|
end
|
@@ -3,7 +3,6 @@ require File.dirname(__FILE__)+"/tests_init"
|
|
3
3
|
require 'test/unit'
|
4
4
|
require 'documents_folders'
|
5
5
|
require 'bookmarks'
|
6
|
-
require 'yaml'
|
7
6
|
|
8
7
|
class XmlMappingAdvancedTest < Test::Unit::TestCase
|
9
8
|
def setup
|
@@ -13,8 +12,12 @@ class XmlMappingAdvancedTest < Test::Unit::TestCase
|
|
13
12
|
Object.send(:remove_const, "Document")
|
14
13
|
Object.send(:remove_const, "Folder")
|
15
14
|
|
16
|
-
$".delete "documents_folders.rb"
|
17
|
-
|
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
|
18
21
|
require 'documents_folders'
|
19
22
|
require 'bookmarks'
|
20
23
|
|
@@ -26,23 +29,20 @@ class XmlMappingAdvancedTest < Test::Unit::TestCase
|
|
26
29
|
end
|
27
30
|
|
28
31
|
def test_read_polymorphic_object
|
29
|
-
|
30
|
-
|
31
|
-
entries
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
name: work
|
44
|
-
name: home
|
45
|
-
EOS
|
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
46
|
end
|
47
47
|
|
48
48
|
def test_write_polymorphic_object
|
data/test/xml_mapping_test.rb
CHANGED
@@ -27,7 +27,10 @@ class XmlMappingTest < Test::Unit::TestCase
|
|
27
27
|
Object.send(:remove_const, "Names1")
|
28
28
|
Object.send(:remove_const, "ReaderTest")
|
29
29
|
Object.send(:remove_const, "WriterTest")
|
30
|
-
|
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
|
31
34
|
$:.unshift File.dirname(__FILE__) # test/unit may have undone this (see test/unit/collector/dir.rb)
|
32
35
|
require 'company'
|
33
36
|
|
@@ -155,7 +158,33 @@ class XmlMappingTest < Test::Unit::TestCase
|
|
155
158
|
assert_equal %w{dingdong2 dingdong3}, xml2.all_xpath("quux").map{|elt|elt.text}
|
156
159
|
end
|
157
160
|
|
158
|
-
|
161
|
+
def test_reader_writer_proc_vs_lambda
|
162
|
+
xml = REXML::Document.new("<test>
|
163
|
+
<proc_2args>proc_2args_text</proc_2args>
|
164
|
+
<lambda_2args>lambda_2args_text</lambda_2args>
|
165
|
+
<proc_3args>proc_3args_text</proc_3args>
|
166
|
+
<lambda_3args>lambda_3args_text</lambda_3args>
|
167
|
+
</test>").root
|
168
|
+
r = ReaderWriterProcVsLambdaTest.load_from_xml xml
|
169
|
+
assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.read
|
170
|
+
assert_nil r.written
|
171
|
+
assert_nil r.proc_2args
|
172
|
+
assert_nil r.lambda_2args
|
173
|
+
assert_equal 'proc_3args_text', r.proc_3args
|
174
|
+
assert_equal 'lambda_3args_text', r.lambda_3args
|
175
|
+
|
176
|
+
r.proc_2args = "proc_2args_text_new"
|
177
|
+
r.lambda_2args = "lambda_2args_text_new"
|
178
|
+
r.proc_3args = "proc_3args_text_new"
|
179
|
+
r.lambda_3args = "lambda_3args_text_new"
|
180
|
+
xml2 = r.save_to_xml
|
181
|
+
assert_equal [:proc_2args, :proc_3args, :lambda_2args, :lambda_3args], r.written
|
182
|
+
assert_nil xml2.first_xpath("proc_2args", :allow_nil=>true)
|
183
|
+
assert_nil xml2.first_xpath("lambda_2args", :allow_nil=>true)
|
184
|
+
assert_equal 'proc_3args_text_new', xml2.first_xpath("proc_3args").text
|
185
|
+
assert_equal 'lambda_3args_text_new', xml2.first_xpath("lambda_3args").text
|
186
|
+
end
|
187
|
+
|
159
188
|
def test_setter_text_node
|
160
189
|
@c.ent2 = "lalala"
|
161
190
|
assert_equal "lalala", REXML::XPath.first(@c.save_to_xml, "arrtest/entry[2]").text
|
data/{README → user_manual.md}
RENAMED
@@ -1,44 +1,377 @@
|
|
1
|
-
|
1
|
+
# XML-MAPPING: XML-to-object (and back) Mapper for Ruby, including XPath Interpreter
|
2
2
|
|
3
3
|
Xml-mapping is an easy to use, extensible library that allows you to
|
4
4
|
semi-automatically map Ruby objects to XML trees and vice versa.
|
5
5
|
|
6
|
-
|
6
|
+
## Download
|
7
7
|
|
8
|
-
For downloading the latest version,
|
8
|
+
For downloading the latest version, git repository access etc. go to:
|
9
9
|
|
10
|
-
|
10
|
+
https://github.com/multi-io/xml-mapping
|
11
11
|
|
12
|
-
|
12
|
+
## Contents of this Document
|
13
13
|
|
14
|
-
- {Example}[
|
15
|
-
- {Single-attribute Nodes}[
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
- {Other Nodes}[
|
20
|
-
|
21
|
-
|
22
|
-
- {Multiple Mappings per Class}[
|
23
|
-
- {Defining your own Node Types}[
|
24
|
-
- {XPath Interpreter}[
|
14
|
+
- {Example}[rdoc-label:label-Example]
|
15
|
+
- {Single-attribute Nodes}[rdoc-label:label-Single-attribute+Nodes]
|
16
|
+
- {Default Values}[rdoc-label:label-Default+Values]
|
17
|
+
- {Single-attribute Nodes with Sub-objects}[rdoc-label:label-Single-attribute+Nodes+with+Sub-objects]
|
18
|
+
- {Attribute Handling Details, Augmenting Existing Classes}[rdoc-label:label-Attribute+Handling+Details%2C+Augmenting+Existing+Classes]
|
19
|
+
- {Other Nodes}[rdoc-label:label-Other+Nodes]
|
20
|
+
- {choice_node}[rdoc-label:label-choice_node]
|
21
|
+
- {Readers/Writers}[rdoc-label:label-Readers%2FWriters]
|
22
|
+
- {Multiple Mappings per Class}[rdoc-label:label-Multiple+Mappings+per+Class]
|
23
|
+
- {Defining your own Node Types}[rdoc-label:label-Defining+your+own+Node+Types]
|
24
|
+
- {XPath Interpreter}[rdoc-label:label-XPath+Interpreter]
|
25
25
|
|
26
|
-
|
26
|
+
## Example
|
27
27
|
|
28
28
|
(example document stolen + extended from
|
29
29
|
http://www.castor.org/xml-mapping.html)
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
### Input Document:
|
32
|
+
|
33
|
+
<?xml version="1.0" encoding="ISO-8859-1"?>
|
34
|
+
|
35
|
+
<Order reference="12343-AHSHE-314159">
|
36
|
+
<Client>
|
37
|
+
<Name>Jean Smith</Name>
|
38
|
+
<Address where="home">
|
39
|
+
<City>San Mateo</City>
|
40
|
+
<State>CA</State>
|
41
|
+
<ZIP>94403</ZIP>
|
42
|
+
<Street>2000, Alameda de las Pulgas</Street>
|
43
|
+
</Address>
|
44
|
+
<Address where="work">
|
45
|
+
<City>San Francisco</City>
|
46
|
+
<State>CA</State>
|
47
|
+
<ZIP>94102</ZIP>
|
48
|
+
<Street>98765, Fulton Street</Street>
|
49
|
+
</Address>
|
50
|
+
</Client>
|
51
|
+
|
52
|
+
<Item reference="RF-0001">
|
53
|
+
<Description>Stuffed Penguin</Description>
|
54
|
+
<Quantity>10</Quantity>
|
55
|
+
<UnitPrice>8.95</UnitPrice>
|
56
|
+
</Item>
|
57
|
+
|
58
|
+
<Item reference="RF-0034">
|
59
|
+
<Description>Chocolate</Description>
|
60
|
+
<Quantity>5</Quantity>
|
61
|
+
<UnitPrice>28.50</UnitPrice>
|
62
|
+
</Item>
|
63
|
+
|
64
|
+
<Item reference="RF-3341">
|
65
|
+
<Description>Cookie</Description>
|
66
|
+
<Quantity>30</Quantity>
|
67
|
+
<UnitPrice>0.85</UnitPrice>
|
68
|
+
</Item>
|
69
|
+
|
70
|
+
<Signed-By>
|
71
|
+
<Signature>
|
72
|
+
<Name>John Doe</Name>
|
73
|
+
<Position>product manager</Position>
|
74
|
+
</Signature>
|
75
|
+
|
76
|
+
<Signature>
|
77
|
+
<Name>Jill Smith</Name>
|
78
|
+
<Position>clerk</Position>
|
79
|
+
</Signature>
|
80
|
+
|
81
|
+
<Signature>
|
82
|
+
<Name>Miles O'Brien</Name>
|
83
|
+
</Signature>
|
84
|
+
</Signed-By>
|
85
|
+
|
86
|
+
</Order>
|
87
|
+
|
88
|
+
### Mapping Class Declaration:
|
89
|
+
|
90
|
+
require 'xml/mapping'
|
91
|
+
|
92
|
+
## forward declarations
|
93
|
+
class Client; end
|
94
|
+
class Address; end
|
95
|
+
class Item; end
|
96
|
+
class Signature; end
|
97
|
+
|
98
|
+
|
99
|
+
class Order
|
100
|
+
include XML::Mapping
|
101
|
+
|
102
|
+
text_node :reference, "@reference"
|
103
|
+
object_node :client, "Client", :class=>Client
|
104
|
+
hash_node :items, "Item", "@reference", :class=>Item
|
105
|
+
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
|
106
|
+
|
107
|
+
def total_price
|
108
|
+
items.values.map{|i| i.total_price}.inject(0){|x,y|x+y}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
class Client
|
114
|
+
include XML::Mapping
|
115
|
+
|
116
|
+
text_node :name, "Name"
|
117
|
+
object_node :home_address, "Address[@where='home']", :class=>Address
|
118
|
+
object_node :work_address, "Address[@where='work']", :class=>Address, :default_value=>nil
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
class Address
|
123
|
+
include XML::Mapping
|
124
|
+
|
125
|
+
text_node :city, "City"
|
126
|
+
text_node :state, "State"
|
127
|
+
numeric_node :zip, "ZIP"
|
128
|
+
text_node :street, "Street"
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
class Item
|
133
|
+
include XML::Mapping
|
134
|
+
|
135
|
+
text_node :descr, "Description"
|
136
|
+
numeric_node :quantity, "Quantity"
|
137
|
+
numeric_node :unit_price, "UnitPrice"
|
138
|
+
|
139
|
+
def total_price
|
140
|
+
quantity*unit_price
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
class Signature
|
146
|
+
include XML::Mapping
|
147
|
+
|
148
|
+
text_node :name, "Name"
|
149
|
+
text_node :position, "Position", :default_value=>"Some Employee"
|
150
|
+
end
|
151
|
+
|
152
|
+
### Usage:
|
153
|
+
|
154
|
+
####read access
|
155
|
+
o=Order.load_from_file("order.xml")
|
156
|
+
=> #<Order:0x007fb01549d438 @signatures=[#<Signature:0x007fb015493b68 @position="product manager", @name="John Doe">, #<Signature:0x007fb015492c68 @position="clerk", @name="Jill Smith">, #<Signature:0x007fb015491a20 @position="Some Employee", @name="Miles O'Brien">], @reference="12343-AHSHE-314159", @client=#<Client:0x007fb01549c998 @work_address=#<Address:0x007fb01549a238 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007fb01549bbb0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>, @items={"RF-0001"=>#<Item:0x007fb0154989b0 @descr="Stuffed Penguin", @quantity=10, @unit_price=8.95>, "RF-0034"=>#<Item:0x007fb015496638 @descr="Chocolate", @quantity=5, @unit_price=28.5>, "RF-3341"=>#<Item:0x007fb015495300 @descr="Cookie", @quantity=30, @unit_price=0.85>}>
|
157
|
+
o.reference
|
158
|
+
=> "12343-AHSHE-314159"
|
159
|
+
o.client
|
160
|
+
=> #<Client:0x007fb01549c998 @work_address=#<Address:0x007fb01549a238 @city="San Francisco", @state="CA", @zip=94102, @street="98765, Fulton Street">, @name="Jean Smith", @home_address=#<Address:0x007fb01549bbb0 @city="San Mateo", @state="CA", @zip=94403, @street="2000, Alameda de las Pulgas">>
|
161
|
+
o.items.keys
|
162
|
+
=> ["RF-0001", "RF-0034", "RF-3341"]
|
163
|
+
o.items["RF-0034"].descr
|
164
|
+
=> "Chocolate"
|
165
|
+
o.items["RF-0034"].total_price
|
166
|
+
=> 142.5
|
167
|
+
o.signatures
|
168
|
+
=> [#<Signature:0x007fb015493b68 @position="product manager", @name="John Doe">, #<Signature:0x007fb015492c68 @position="clerk", @name="Jill Smith">, #<Signature:0x007fb015491a20 @position="Some Employee", @name="Miles O'Brien">]
|
169
|
+
o.signatures[2].name
|
170
|
+
=> "Miles O'Brien"
|
171
|
+
o.signatures[2].position
|
172
|
+
=> "Some Employee"
|
173
|
+
## default value was set
|
174
|
+
|
175
|
+
o.total_price
|
176
|
+
=> 257.5
|
177
|
+
|
178
|
+
####write access
|
179
|
+
o.client.name="James T. Kirk"
|
180
|
+
o.items['RF-4711'] = Item.new
|
181
|
+
o.items['RF-4711'].descr = 'power transfer grid'
|
182
|
+
o.items['RF-4711'].quantity = 2
|
183
|
+
o.items['RF-4711'].unit_price = 29.95
|
184
|
+
|
185
|
+
s=Signature.new
|
186
|
+
s.name='Harry Smith'
|
187
|
+
s.position='general manager'
|
188
|
+
o.signatures << s
|
189
|
+
xml=o.save_to_xml #convert to REXML node; there's also o.save_to_file(name)
|
190
|
+
=> <order reference='12343-AHSHE-314159'> ... </>
|
191
|
+
xml.write($stdout,2)
|
192
|
+
<order reference='12343-AHSHE-314159'>
|
193
|
+
<Client>
|
194
|
+
<Name>
|
195
|
+
James T. Kirk
|
196
|
+
</Name>
|
197
|
+
<Address where='home'>
|
198
|
+
<City>
|
199
|
+
San Mateo
|
200
|
+
</City>
|
201
|
+
<State>
|
202
|
+
CA
|
203
|
+
</State>
|
204
|
+
<ZIP>
|
205
|
+
94403
|
206
|
+
</ZIP>
|
207
|
+
<Street>
|
208
|
+
2000, Alameda de las Pulgas
|
209
|
+
</Street>
|
210
|
+
</Address>
|
211
|
+
<Address where='work'>
|
212
|
+
<City>
|
213
|
+
San Francisco
|
214
|
+
</City>
|
215
|
+
<State>
|
216
|
+
CA
|
217
|
+
</State>
|
218
|
+
<ZIP>
|
219
|
+
94102
|
220
|
+
</ZIP>
|
221
|
+
<Street>
|
222
|
+
98765, Fulton Street
|
223
|
+
</Street>
|
224
|
+
</Address>
|
225
|
+
</Client>
|
226
|
+
<Item reference='RF-0001'>
|
227
|
+
<Description>
|
228
|
+
Stuffed Penguin
|
229
|
+
</Description>
|
230
|
+
<Quantity>
|
231
|
+
10
|
232
|
+
</Quantity>
|
233
|
+
<UnitPrice>
|
234
|
+
8.95
|
235
|
+
</UnitPrice>
|
236
|
+
</Item>
|
237
|
+
<Item reference='RF-0034'>
|
238
|
+
<Description>
|
239
|
+
Chocolate
|
240
|
+
</Description>
|
241
|
+
<Quantity>
|
242
|
+
5
|
243
|
+
</Quantity>
|
244
|
+
<UnitPrice>
|
245
|
+
28.5
|
246
|
+
</UnitPrice>
|
247
|
+
</Item>
|
248
|
+
<Item reference='RF-3341'>
|
249
|
+
<Description>
|
250
|
+
Cookie
|
251
|
+
</Description>
|
252
|
+
<Quantity>
|
253
|
+
30
|
254
|
+
</Quantity>
|
255
|
+
<UnitPrice>
|
256
|
+
0.85
|
257
|
+
</UnitPrice>
|
258
|
+
</Item>
|
259
|
+
<Item reference='RF-4711'>
|
260
|
+
<Description>
|
261
|
+
power transfer grid
|
262
|
+
</Description>
|
263
|
+
<Quantity>
|
264
|
+
2
|
265
|
+
</Quantity>
|
266
|
+
<UnitPrice>
|
267
|
+
29.95
|
268
|
+
</UnitPrice>
|
269
|
+
</Item>
|
270
|
+
<Signed-By>
|
271
|
+
<Signature>
|
272
|
+
<Name>
|
273
|
+
John Doe
|
274
|
+
</Name>
|
275
|
+
<Position>
|
276
|
+
product manager
|
277
|
+
</Position>
|
278
|
+
</Signature>
|
279
|
+
<Signature>
|
280
|
+
<Name>
|
281
|
+
Jill Smith
|
282
|
+
</Name>
|
283
|
+
<Position>
|
284
|
+
clerk
|
285
|
+
</Position>
|
286
|
+
</Signature>
|
287
|
+
<Signature>
|
288
|
+
<Name>
|
289
|
+
Miles O'Brien
|
290
|
+
</Name>
|
291
|
+
</Signature>
|
292
|
+
<Signature>
|
293
|
+
<Name>
|
294
|
+
Harry Smith
|
295
|
+
</Name>
|
296
|
+
<Position>
|
297
|
+
general manager
|
298
|
+
</Position>
|
299
|
+
</Signature>
|
300
|
+
</Signed-By>
|
301
|
+
</order>
|
302
|
+
|
303
|
+
|
304
|
+
####Starting a new order from scratch
|
305
|
+
o = Order.new
|
306
|
+
=> #<Order:0x007fb0153bfef8 @signatures=[]>
|
307
|
+
## attributes with default values (here: signatures) are set
|
308
|
+
## automatically
|
309
|
+
|
310
|
+
xml=o.save_to_xml
|
311
|
+
XML::MappingError: no value, and no default value, for attribute: reference
|
312
|
+
from /home/olaf/xml-mapping/lib/xml/mapping/base.rb:712:in `obj_to_xml'
|
313
|
+
from /home/olaf/xml-mapping/lib/xml/mapping/base.rb:218:in `block in fill_into_xml'
|
314
|
+
from /home/olaf/xml-mapping/lib/xml/mapping/base.rb:217:in `each'
|
315
|
+
from /home/olaf/xml-mapping/lib/xml/mapping/base.rb:217:in `fill_into_xml'
|
316
|
+
from /home/olaf/xml-mapping/lib/xml/mapping/base.rb:229:in `save_to_xml'
|
317
|
+
## can't save as long as there are still unset attributes without
|
318
|
+
## default values
|
319
|
+
|
320
|
+
o.reference = "FOOBAR-1234"
|
321
|
+
|
322
|
+
o.client = Client.new
|
323
|
+
o.client.name = 'Ford Prefect'
|
324
|
+
o.client.home_address = Address.new
|
325
|
+
o.client.home_address.street = '42 Park Av.'
|
326
|
+
o.client.home_address.city = 'small planet'
|
327
|
+
o.client.home_address.zip = 17263
|
328
|
+
o.client.home_address.state = 'Betelgeuse system'
|
329
|
+
|
330
|
+
o.items={'XY-42' => Item.new}
|
331
|
+
o.items['XY-42'].descr = 'improbability drive'
|
332
|
+
o.items['XY-42'].quantity = 3
|
333
|
+
o.items['XY-42'].unit_price = 299.95
|
334
|
+
|
335
|
+
xml=o.save_to_xml
|
336
|
+
xml.write($stdout,2)
|
337
|
+
|
338
|
+
<order reference='FOOBAR-1234'>
|
339
|
+
<Client>
|
340
|
+
<Name>
|
341
|
+
Ford Prefect
|
342
|
+
</Name>
|
343
|
+
<Address where='home'>
|
344
|
+
<City>
|
345
|
+
small planet
|
346
|
+
</City>
|
347
|
+
<State>
|
348
|
+
Betelgeuse system
|
349
|
+
</State>
|
350
|
+
<ZIP>
|
351
|
+
17263
|
352
|
+
</ZIP>
|
353
|
+
<Street>
|
354
|
+
42 Park Av.
|
355
|
+
</Street>
|
356
|
+
</Address>
|
357
|
+
</Client>
|
358
|
+
<Item reference='XY-42'>
|
359
|
+
<Description>
|
360
|
+
improbability drive
|
361
|
+
</Description>
|
362
|
+
<Quantity>
|
363
|
+
3
|
364
|
+
</Quantity>
|
365
|
+
<UnitPrice>
|
366
|
+
299.95
|
367
|
+
</UnitPrice>
|
368
|
+
</Item>
|
369
|
+
</order>
|
370
|
+
## the root element name when saving an object to XML will by default
|
371
|
+
## be derived from the class name (in this example, "Order" became
|
372
|
+
## "order"). This can be overridden on a per-class basis; see
|
373
|
+
## XML::Mapping::ClassMethods#root_element_name for details.
|
374
|
+
|
42
375
|
|
43
376
|
As shown in the example, you have to include XML::Mapping into a class
|
44
377
|
to turn it into a "mapping class". There are no other restrictions
|
@@ -55,18 +388,18 @@ from the body of the class definition to define instance attributes
|
|
55
388
|
that are automatically and bidirectionally mapped to subtrees of the
|
56
389
|
XML element an instance of the class is mapped to.
|
57
390
|
|
58
|
-
|
391
|
+
## Single-attribute Nodes
|
59
392
|
|
60
393
|
For example, in the definition
|
61
394
|
|
62
|
-
|
63
|
-
|
395
|
+
class Address
|
396
|
+
include XML::Mapping
|
64
397
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
398
|
+
text_node :city, "City"
|
399
|
+
text_node :state, "State"
|
400
|
+
numeric_node :zip, "ZIP"
|
401
|
+
text_node :street, "Street"
|
402
|
+
end
|
70
403
|
|
71
404
|
the first call to #text_node creates an attribute named "city" which
|
72
405
|
is mapped to the text of the XML child element defined by the XPath
|
@@ -90,7 +423,7 @@ node". All node types that come with xml-mapping except one
|
|
90
423
|
nodes.
|
91
424
|
|
92
425
|
|
93
|
-
|
426
|
+
### Default Values
|
94
427
|
|
95
428
|
For each single-attribute node you may define a <i>default value</i>
|
96
429
|
which will be set if there was no value defined for the attribute in
|
@@ -98,43 +431,43 @@ the XML source.
|
|
98
431
|
|
99
432
|
From the example:
|
100
433
|
|
101
|
-
|
102
|
-
|
434
|
+
class Signature
|
435
|
+
include XML::Mapping
|
103
436
|
|
104
|
-
|
105
|
-
|
437
|
+
text_node :position, "Position", :default_value=>"Some Employee"
|
438
|
+
end
|
106
439
|
|
107
440
|
The semantics of default values are as follows:
|
108
441
|
|
109
442
|
- when creating a new instance from scratch:
|
110
443
|
|
111
|
-
|
444
|
+
- attributes with default values are set to their default values
|
112
445
|
|
113
|
-
|
446
|
+
- attributes without default values are left unset
|
114
447
|
|
115
448
|
(when defining your own initializer, you'll have to call the
|
116
449
|
inherited _initialize_ method in order to get this behaviour)
|
117
450
|
|
118
451
|
- when loading an instance from an XML document:
|
119
452
|
|
120
|
-
|
121
|
-
|
453
|
+
- attributes without default values that are not represented in
|
454
|
+
the XML raise an error
|
122
455
|
|
123
|
-
|
124
|
-
|
456
|
+
- attributes with default values that are not represented in the
|
457
|
+
XML are set to their default values
|
125
458
|
|
126
|
-
|
127
|
-
|
459
|
+
- all other attributes are set to their respective values as
|
460
|
+
present in the XML
|
128
461
|
|
129
462
|
|
130
463
|
- when saving an instance to an XML document:
|
131
464
|
|
132
|
-
|
465
|
+
- unset attributes without default values raise an error
|
133
466
|
|
134
|
-
|
135
|
-
|
467
|
+
- attributes with default values that are set to their default
|
468
|
+
values are not saved
|
136
469
|
|
137
|
-
|
470
|
+
- all other attributes are saved
|
138
471
|
|
139
472
|
|
140
473
|
This implies that:
|
@@ -147,14 +480,14 @@ This implies that:
|
|
147
480
|
|
148
481
|
|
149
482
|
|
150
|
-
|
483
|
+
### Single-attribute Nodes with Sub-objects
|
151
484
|
|
152
485
|
Single-attribute nodes of type +array_node+, +hash_node+, and
|
153
486
|
+object_node+ recursively map one or more subtrees of their XML to
|
154
487
|
sub-objects (e.g. array elements or hash values) of their
|
155
488
|
attribute. For example, with the line
|
156
489
|
|
157
|
-
|
490
|
+
array_node :signatures, "Signed-By", "Signature", :class=>Signature, :default_value=>[]
|
158
491
|
|
159
492
|
, an attribute named "signatures" is added to the surrounding class
|
160
493
|
(here: +Order+); the attribute will be an array whose elements
|
@@ -174,17 +507,17 @@ attribute contains an array with 3 +Signature+ instances (let's call
|
|
174
507
|
them <tt>sig1</tt>, <tt>sig2</tt>, and <tt>sig3</tt>) in it, it will
|
175
508
|
be marshalled to an XML tree that looks like this:
|
176
509
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
510
|
+
<Signed-By>
|
511
|
+
<Signature>
|
512
|
+
[marshalled object sig1]
|
513
|
+
</Signature>
|
514
|
+
<Signature>
|
515
|
+
[marshalled object sig2]
|
516
|
+
</Signature>
|
517
|
+
<Signature>
|
518
|
+
[marshalled object sig3]
|
519
|
+
</Signature>
|
520
|
+
</Signed-By>
|
188
521
|
|
189
522
|
Internally, each +Signature+ instance is stored into its
|
190
523
|
<tt><Signature></tt> sub-element by calling
|
@@ -212,16 +545,59 @@ strings or numbers as sub-objects of attributes of +array_node+,
|
|
212
545
|
+hash_node+, or +object_node+ nodes. For example, say you have an XML
|
213
546
|
document like this one:
|
214
547
|
|
215
|
-
|
548
|
+
<?xml version="1.0" encoding="ISO-8859-1"?>
|
549
|
+
|
550
|
+
<people>
|
551
|
+
<names>
|
552
|
+
<name>Jim</name>
|
553
|
+
<name>Susan</name>
|
554
|
+
<name>Herbie</name>
|
555
|
+
<name>Nancy</name>
|
556
|
+
</names>
|
557
|
+
</people>
|
216
558
|
|
217
559
|
, and you want to map all the names to a string array attribute
|
218
560
|
+names+, you could do it like this:
|
219
561
|
|
220
|
-
|
562
|
+
require 'xml/mapping'
|
563
|
+
class People
|
564
|
+
include XML::Mapping
|
565
|
+
array_node :names, "names", "name", :class=>String
|
566
|
+
end
|
221
567
|
|
222
568
|
usage:
|
223
569
|
|
224
|
-
|
570
|
+
ppl=People.load_from_file("stringarray.xml")
|
571
|
+
=> #<People:0x007fb015431a58 @names=["Jim", "Susan", "Herbie", "Nancy"]>
|
572
|
+
ppl.names
|
573
|
+
=> ["Jim", "Susan", "Herbie", "Nancy"]
|
574
|
+
|
575
|
+
ppl.names.concat ["Mary","Arnold"]
|
576
|
+
=> ["Jim", "Susan", "Herbie", "Nancy", "Mary", "Arnold"]
|
577
|
+
ppl.save_to_xml.write $stdout,2
|
578
|
+
|
579
|
+
<people>
|
580
|
+
<names>
|
581
|
+
<name>
|
582
|
+
Jim
|
583
|
+
</name>
|
584
|
+
<name>
|
585
|
+
Susan
|
586
|
+
</name>
|
587
|
+
<name>
|
588
|
+
Herbie
|
589
|
+
</name>
|
590
|
+
<name>
|
591
|
+
Nancy
|
592
|
+
</name>
|
593
|
+
<name>
|
594
|
+
Mary
|
595
|
+
</name>
|
596
|
+
<name>
|
597
|
+
Arnold
|
598
|
+
</name>
|
599
|
+
</names>
|
600
|
+
</people>
|
225
601
|
|
226
602
|
As a side node, this feature actually makes +text_node+ and
|
227
603
|
+numeric_node+ special cases of +object_node+. For example,
|
@@ -229,7 +605,7 @@ As a side node, this feature actually makes +text_node+ and
|
|
229
605
|
"path", :class=>String</tt>.
|
230
606
|
|
231
607
|
|
232
|
-
|
608
|
+
#### Polymorphic Sub-objects, Marshallers/Unmarshallers
|
233
609
|
|
234
610
|
Besides the <tt>:class</tt> keyword argument, there are alternative
|
235
611
|
ways for a single-attribute node with sub-objects to specify the way
|
@@ -244,15 +620,105 @@ recursive set of named "documents" and "folders", where folders hold a
|
|
244
620
|
set of entries, each of which may again be either a document or a
|
245
621
|
folder:
|
246
622
|
|
247
|
-
|
623
|
+
<?xml version="1.0" encoding="ISO-8859-1"?>
|
624
|
+
|
625
|
+
<folder name="home">
|
626
|
+
<document name="plan">
|
627
|
+
<contents> inhale, exhale</contents>
|
628
|
+
</document>
|
629
|
+
|
630
|
+
<folder name="work">
|
631
|
+
<folder name="xml-mapping">
|
632
|
+
<document name="README">
|
633
|
+
<contents>foo bar baz</contents>
|
634
|
+
</document>
|
635
|
+
</folder>
|
636
|
+
</folder>
|
637
|
+
|
638
|
+
</folder>
|
248
639
|
|
249
640
|
This can be mapped to Ruby like this:
|
250
641
|
|
251
|
-
|
642
|
+
require 'xml/mapping'
|
643
|
+
|
644
|
+
class Entry
|
645
|
+
include XML::Mapping
|
646
|
+
|
647
|
+
text_node :name, "@name"
|
648
|
+
end
|
649
|
+
|
650
|
+
|
651
|
+
class Document <Entry
|
652
|
+
include XML::Mapping
|
653
|
+
|
654
|
+
text_node :contents, "contents"
|
655
|
+
end
|
656
|
+
|
657
|
+
|
658
|
+
class Folder <Entry
|
659
|
+
include XML::Mapping
|
660
|
+
|
661
|
+
array_node :entries, "document|folder", :default_value=>[]
|
662
|
+
|
663
|
+
def [](name)
|
664
|
+
entries.select{|e|e.name==name}[0]
|
665
|
+
end
|
666
|
+
|
667
|
+
def append(name,entry)
|
668
|
+
entries << entry
|
669
|
+
entry.name = name
|
670
|
+
entry
|
671
|
+
end
|
672
|
+
end
|
252
673
|
|
253
674
|
Usage:
|
254
675
|
|
255
|
-
|
676
|
+
|
677
|
+
root = XML::Mapping.load_object_from_file "documents_folders.xml"
|
678
|
+
=> #<Folder:0x007fb015437098 @entries=[#<Document:0x007fb015436148 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007fb0154352c0 @entries=[#<Folder:0x007fb015434550 @entries=[#<Document:0x007fb015432e30 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">], @name="home">
|
679
|
+
root.name
|
680
|
+
=> "home"
|
681
|
+
root.entries
|
682
|
+
=> [#<Document:0x007fb015436148 @name="plan", @contents=" inhale, exhale">, #<Folder:0x007fb0154352c0 @entries=[#<Folder:0x007fb015434550 @entries=[#<Document:0x007fb015432e30 @name="README", @contents="foo bar baz">], @name="xml-mapping">], @name="work">]
|
683
|
+
|
684
|
+
root.append "etc", Folder.new
|
685
|
+
root["etc"].append "passwd", Document.new
|
686
|
+
root["etc"]["passwd"].contents = "foo:x:2:2:/bin/sh"
|
687
|
+
root["etc"].append "hosts", Document.new
|
688
|
+
root["etc"]["hosts"].contents = "127.0.0.1 localhost"
|
689
|
+
|
690
|
+
xml = root.save_to_xml
|
691
|
+
=> <folder name='home'> ... </>
|
692
|
+
xml.write $stdout,2
|
693
|
+
|
694
|
+
<folder name='home'>
|
695
|
+
<document name='plan'>
|
696
|
+
<contents>
|
697
|
+
inhale, exhale
|
698
|
+
</contents>
|
699
|
+
</document>
|
700
|
+
<folder name='work'>
|
701
|
+
<folder name='xml-mapping'>
|
702
|
+
<document name='README'>
|
703
|
+
<contents>
|
704
|
+
foo bar baz
|
705
|
+
</contents>
|
706
|
+
</document>
|
707
|
+
</folder>
|
708
|
+
</folder>
|
709
|
+
<folder name='etc'>
|
710
|
+
<document name='passwd'>
|
711
|
+
<contents>
|
712
|
+
foo:x:2:2:/bin/sh
|
713
|
+
</contents>
|
714
|
+
</document>
|
715
|
+
<document name='hosts'>
|
716
|
+
<contents>
|
717
|
+
127.0.0.1 localhost
|
718
|
+
</contents>
|
719
|
+
</document>
|
720
|
+
</folder>
|
721
|
+
</folder>
|
256
722
|
|
257
723
|
As you see, the <tt>Folder#entries</tt> attribute is mapped via an
|
258
724
|
array_node that does not specify a <tt>:class</tt> or anything else to
|
@@ -297,7 +763,15 @@ extend the +Signature+ class from the initial example to include the
|
|
297
763
|
date on which the signature was created. We want the new XML
|
298
764
|
representation of such a signature to look like this:
|
299
765
|
|
300
|
-
|
766
|
+
<Signature>
|
767
|
+
<Name>John Doe</Name>
|
768
|
+
<Position>product manager</Position>
|
769
|
+
<signed-on>
|
770
|
+
<day>13</day>
|
771
|
+
<month>2</month>
|
772
|
+
<year>2005</year>
|
773
|
+
</signed-on>
|
774
|
+
</Signature>
|
301
775
|
|
302
776
|
So, a new "signed-on" element was added that holds the day, month, and
|
303
777
|
year. In the +Signature+ instance in Ruby, we want the date to be
|
@@ -313,7 +787,31 @@ here[aref:definingnodes]). The fastest, most ad-hoc way to achieve
|
|
313
787
|
what we want are :marshaller and :unmarshaller keyword arguments, like
|
314
788
|
this:
|
315
789
|
|
316
|
-
|
790
|
+
require 'xml/mapping'
|
791
|
+
require 'xml/xxpath_methods'
|
792
|
+
|
793
|
+
class Signature
|
794
|
+
include XML::Mapping
|
795
|
+
|
796
|
+
text_node :name, "Name"
|
797
|
+
text_node :position, "Position", :default_value=>"Some Employee"
|
798
|
+
object_node :signed_on, "signed-on",
|
799
|
+
:unmarshaller=>proc{|xml|
|
800
|
+
y,m,d = [xml.first_xpath("year").text.to_i,
|
801
|
+
xml.first_xpath("month").text.to_i,
|
802
|
+
xml.first_xpath("day").text.to_i]
|
803
|
+
Time.local(y,m,d)
|
804
|
+
},
|
805
|
+
:marshaller=>proc{|xml,value|
|
806
|
+
e = xml.elements.add; e.name = "year"; e.text = value.year
|
807
|
+
e = xml.elements.add; e.name = "month"; e.text = value.month
|
808
|
+
e = xml.elements.add; e.name = "day"; e.text = value.day
|
809
|
+
|
810
|
+
# xml.first("year",:ensure_created=>true).text = value.year
|
811
|
+
# xml.first("month",:ensure_created=>true).text = value.month
|
812
|
+
# xml.first("day",:ensure_created=>true).text = value.day
|
813
|
+
}
|
814
|
+
end
|
317
815
|
|
318
816
|
The <tt>:unmarshaller</tt> proc will be called whenever a +Signature+
|
319
817
|
instance is being read in from an XML source. The +xml+ argument
|
@@ -353,9 +851,9 @@ possible with all single-attribute nodes with sub-objects, i.e. with
|
|
353
851
|
a whole array of date values, you could use +array_node+ with the same
|
354
852
|
:marshaller/:unmarshaller procs as above, for example:
|
355
853
|
|
356
|
-
|
357
|
-
|
358
|
-
|
854
|
+
array_node :birthdays, "birthdays", "birthday",
|
855
|
+
:unmarshaller=> <as above>,
|
856
|
+
:marshaller=> <as above>
|
359
857
|
|
360
858
|
You can see that :marshaller/:unmarshaller procs give you more
|
361
859
|
flexibility, but they also impose more work because you essentially
|
@@ -383,12 +881,12 @@ functionality of a node type while retaining another part.
|
|
383
881
|
|
384
882
|
|
385
883
|
|
386
|
-
|
884
|
+
### Attribute Handling Details, Augmenting Existing Classes
|
387
885
|
|
388
886
|
I'll shed some more light on how single-attribute nodes add mapped
|
389
887
|
attributes to Ruby classes. An attribute declaration like
|
390
888
|
|
391
|
-
|
889
|
+
text_node :city, "City"
|
392
890
|
|
393
891
|
maps some portion of the XML tree (here: the "City" sub-element) to an
|
394
892
|
attribute (here: "city") of the class whose body the declaration
|
@@ -418,7 +916,41 @@ scratch, you can also take existing classes that contain some
|
|
418
916
|
a simple example, let's augment Ruby's "Time" class with node
|
419
917
|
declarations that declare XML mappings for the day, month etc. fields:
|
420
918
|
|
421
|
-
|
919
|
+
class Time
|
920
|
+
include XML::Mapping
|
921
|
+
|
922
|
+
numeric_node :year, "year"
|
923
|
+
numeric_node :month, "month"
|
924
|
+
numeric_node :day, "mday"
|
925
|
+
numeric_node :hour, "hours"
|
926
|
+
numeric_node :min, "minutes"
|
927
|
+
numeric_node :sec, "seconds"
|
928
|
+
end
|
929
|
+
|
930
|
+
|
931
|
+
nowxml=Time.now.save_to_xml
|
932
|
+
=> <time> ... </>
|
933
|
+
nowxml.write($stdout,2)
|
934
|
+
<time>
|
935
|
+
<year>
|
936
|
+
2014
|
937
|
+
</year>
|
938
|
+
<month>
|
939
|
+
9
|
940
|
+
</month>
|
941
|
+
<mday>
|
942
|
+
19
|
943
|
+
</mday>
|
944
|
+
<hours>
|
945
|
+
14
|
946
|
+
</hours>
|
947
|
+
<minutes>
|
948
|
+
1
|
949
|
+
</minutes>
|
950
|
+
<seconds>
|
951
|
+
43
|
952
|
+
</seconds>
|
953
|
+
</time>
|
422
954
|
|
423
955
|
Here XML mappings are defined for the existing fields +year+, +month+
|
424
956
|
etc. Xml-mapping noticed that the getter methods for those attributes
|
@@ -445,10 +977,21 @@ variables, so the setter methods don't really change the time of the
|
|
445
977
|
+Time+ object. So we have to redefine +load_from_xml+ for the +Time+
|
446
978
|
class:
|
447
979
|
|
448
|
-
|
980
|
+
|
981
|
+
def Time.load_from_xml(xml, options={:mapping=>:_default})
|
982
|
+
year,month,day,hour,min,sec =
|
983
|
+
[xml.first_xpath("year").text.to_i,
|
984
|
+
xml.first_xpath("month").text.to_i,
|
985
|
+
xml.first_xpath("mday").text.to_i,
|
986
|
+
xml.first_xpath("hours").text.to_i,
|
987
|
+
xml.first_xpath("minutes").text.to_i,
|
988
|
+
xml.first_xpath("seconds").text.to_i]
|
989
|
+
Time.local(year,month,day,hour,min,sec)
|
990
|
+
end
|
991
|
+
|
449
992
|
|
450
993
|
|
451
|
-
|
994
|
+
## Other Nodes
|
452
995
|
|
453
996
|
All nodes I've shown so far (node types text_node, numeric_node,
|
454
997
|
boolean_node, object_node, array_node, and hash_node) were
|
@@ -457,7 +1000,7 @@ of such a node is an attribute name, and the attribute of that name is
|
|
457
1000
|
the only piece of the state of instances of the node's mapping class
|
458
1001
|
that gets read/written by the node.
|
459
1002
|
|
460
|
-
|
1003
|
+
### choice_node
|
461
1004
|
|
462
1005
|
There is one node type distributed with xml-mapping that is not a
|
463
1006
|
single-attribute node: +choice_node+. A +choice_node+ allows you to
|
@@ -477,7 +1020,26 @@ objects that have either a single author (contained in an "author" XML
|
|
477
1020
|
attribute) or several "contributors" (contained in a sequence of
|
478
1021
|
"contr" XML elements):
|
479
1022
|
|
480
|
-
|
1023
|
+
|
1024
|
+
class Publication
|
1025
|
+
include XML::Mapping
|
1026
|
+
|
1027
|
+
choice_node :if, '@author', :then, (text_node :author, '@author'),
|
1028
|
+
:elsif, 'contr', :then, (array_node :contributors, 'contr', :class=>String)
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
### usage
|
1032
|
+
|
1033
|
+
p1 = Publication.load_from_xml(REXML::Document.new('<publication author="Jim"/>').root)
|
1034
|
+
=> #<Publication:0x007fb01548e668 @author="Jim">
|
1035
|
+
|
1036
|
+
p2 = Publication.load_from_xml(REXML::Document.new('
|
1037
|
+
<publication>
|
1038
|
+
<contr>Chris</contr>
|
1039
|
+
<contr>Mel</contr>
|
1040
|
+
<contr>Toby</contr>
|
1041
|
+
</publication>').root)
|
1042
|
+
=> #<Publication:0x007fb01547ef38 @contributors=["Chris", "Mel", "Toby"]>
|
481
1043
|
|
482
1044
|
The symbols :if, :then, and :elsif (but not :else -- see below) in the
|
483
1045
|
+choice_node+'s node factory method call are ignored; they may be
|
@@ -519,7 +1081,33 @@ person should be stored alternatively in a sub-element named +name+,
|
|
519
1081
|
or an attribute named +name+, or in the text of the +person+ element
|
520
1082
|
itself. You can achieve this with +choice_node+ like this:
|
521
1083
|
|
522
|
-
|
1084
|
+
|
1085
|
+
class Person
|
1086
|
+
include XML::Mapping
|
1087
|
+
|
1088
|
+
choice_node :if, 'name', :then, (text_node :name, 'name'),
|
1089
|
+
:elsif, '@name', :then, (text_node :name, '@name'),
|
1090
|
+
:else, (text_node :name, '.')
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
### usage
|
1094
|
+
|
1095
|
+
p1 = Person.load_from_xml(REXML::Document.new('<person name="Jim"/>').root)
|
1096
|
+
=> #<Person:0x007fb014e95298 @name="Jim">
|
1097
|
+
|
1098
|
+
p2 = Person.load_from_xml(REXML::Document.new('<person><name>James</name></person>').root)
|
1099
|
+
=> #<Person:0x007fb014e847b8 @name="James">
|
1100
|
+
|
1101
|
+
p3 = Person.load_from_xml(REXML::Document.new('<person>Suzy</person>').root)
|
1102
|
+
=> #<Person:0x007fb014e6c550 @name="Suzy">
|
1103
|
+
|
1104
|
+
|
1105
|
+
p1.save_to_xml.write($stdout)
|
1106
|
+
<person><name>Jim</name></person>
|
1107
|
+
p2.save_to_xml.write($stdout)
|
1108
|
+
<person><name>James</name></person>
|
1109
|
+
p3.save_to_xml.write($stdout)
|
1110
|
+
<person><name>Suzy</name></person>
|
523
1111
|
|
524
1112
|
Here all sub-nodes of the choice_nodes are single-attribute nodes
|
525
1113
|
(text_nodes) with the same attribute (+name+). As you see, when
|
@@ -528,7 +1116,7 @@ sub-element. Of course, this is because that alternative appears first
|
|
528
1116
|
in the choice_node.
|
529
1117
|
|
530
1118
|
|
531
|
-
|
1119
|
+
### Readers/Writers
|
532
1120
|
|
533
1121
|
Finally, _all_ nodes support keyword arguments :reader and :writer
|
534
1122
|
which allow you to extend or completely override the reading and/or
|
@@ -545,7 +1133,25 @@ The :reader proc is for reading (from the XML into the object), the
|
|
545
1133
|
|
546
1134
|
Here's a (really contrived) example:
|
547
1135
|
|
548
|
-
|
1136
|
+
|
1137
|
+
class Foo
|
1138
|
+
include XML::Mapping
|
1139
|
+
|
1140
|
+
text_node :name, "@name", :reader=>proc{|obj,xml,default_reader|
|
1141
|
+
default_reader.call(obj,xml)
|
1142
|
+
obj.name += xml.attributes['more']
|
1143
|
+
},
|
1144
|
+
:writer=>proc{|obj,xml|
|
1145
|
+
xml.attributes['bar'] = "hi #{obj.name} ho"
|
1146
|
+
}
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
f = Foo.load_from_xml(REXML::Document.new('<foo name="Jim" more="XYZ"/>').root)
|
1150
|
+
=> #<Foo:0x007fb015460c40 @name="JimXYZ">
|
1151
|
+
|
1152
|
+
xml = f.save_to_xml
|
1153
|
+
xml.write $stdout,2
|
1154
|
+
<foo bar='hi JimXYZ ho'/>
|
549
1155
|
|
550
1156
|
So there's a "Foo" class with a text_node that would by default
|
551
1157
|
(without the :reader and :writer proc) map the Ruby attribute "name"
|
@@ -573,14 +1179,14 @@ As a special convention, if you specify both a :reader and a :writer
|
|
573
1179
|
for a node, and in both cases you do /not/ call the default behaviour,
|
574
1180
|
then you should use the generic node type +node+, e.g.:
|
575
1181
|
|
576
|
-
|
577
|
-
|
1182
|
+
class SomeClass
|
1183
|
+
include XML::Mapping
|
578
1184
|
|
579
|
-
|
1185
|
+
....
|
580
1186
|
|
581
|
-
|
582
|
-
|
583
|
-
|
1187
|
+
node :reader=>proc{|obj,xml| ...},
|
1188
|
+
:writer=>proc{|obj,xml| ...}
|
1189
|
+
end
|
584
1190
|
|
585
1191
|
(since you're completely replacing both the reading and the writing
|
586
1192
|
functionality, you're effectively replacing all the functionality of
|
@@ -608,7 +1214,7 @@ the (sensibly parameterized) code from your readers/writers to your
|
|
608
1214
|
node types.
|
609
1215
|
|
610
1216
|
|
611
|
-
|
1217
|
+
## Multiple Mappings per Class
|
612
1218
|
|
613
1219
|
Sometimes you might want to represent the same Ruby object in multiple
|
614
1220
|
alternative ways in XML. For example, the name of a "Person" object
|
@@ -635,7 +1241,120 @@ In the following example, we define two mappings (the default one and
|
|
635
1241
|
a mapping named <tt>:other</tt>) for +Person+ objects with a name, an
|
636
1242
|
age and an address:
|
637
1243
|
|
638
|
-
|
1244
|
+
require 'xml/mapping'
|
1245
|
+
|
1246
|
+
class Address; end
|
1247
|
+
|
1248
|
+
class Person
|
1249
|
+
include XML::Mapping
|
1250
|
+
|
1251
|
+
# the default mapping. Stores the name and age in XML attributes,
|
1252
|
+
# and the address in a sub-element "address".
|
1253
|
+
|
1254
|
+
text_node :name, "@name"
|
1255
|
+
numeric_node :age, "@age"
|
1256
|
+
object_node :address, "address", :class=>Address
|
1257
|
+
|
1258
|
+
use_mapping :other
|
1259
|
+
|
1260
|
+
# the ":other" mapping. Non-default root element name; name and age
|
1261
|
+
# stored in XML elements; address stored in the person's element
|
1262
|
+
# itself
|
1263
|
+
|
1264
|
+
root_element_name "individual"
|
1265
|
+
text_node :name, "name"
|
1266
|
+
numeric_node :age, "age"
|
1267
|
+
object_node :address, ".", :class=>Address
|
1268
|
+
|
1269
|
+
# you could also specify the mapping on a per-node basis with the
|
1270
|
+
# :mapping option, e.g.:
|
1271
|
+
#
|
1272
|
+
# numeric_node :age, "age", :mapping=>:other
|
1273
|
+
end
|
1274
|
+
|
1275
|
+
|
1276
|
+
class Address
|
1277
|
+
include XML::Mapping
|
1278
|
+
|
1279
|
+
# the default mapping.
|
1280
|
+
|
1281
|
+
text_node :street, "street"
|
1282
|
+
numeric_node :number, "number"
|
1283
|
+
text_node :city, "city"
|
1284
|
+
numeric_node :zip, "zip"
|
1285
|
+
|
1286
|
+
use_mapping :other
|
1287
|
+
|
1288
|
+
# the ":other" mapping.
|
1289
|
+
|
1290
|
+
text_node :street, "street-name"
|
1291
|
+
numeric_node :number, "street-name/@number"
|
1292
|
+
text_node :city, "city-name"
|
1293
|
+
numeric_node :zip, "city-name/@zip-code"
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
|
1297
|
+
### usage
|
1298
|
+
|
1299
|
+
## XML representation of a person in the default mapping
|
1300
|
+
xml = REXML::Document.new('
|
1301
|
+
<person name="Suzy" age="28">
|
1302
|
+
<address>
|
1303
|
+
<street>Abbey Road</street>
|
1304
|
+
<number>72</number>
|
1305
|
+
<city>London</city>
|
1306
|
+
<zip>18827</zip>
|
1307
|
+
</address>
|
1308
|
+
</person>').root
|
1309
|
+
|
1310
|
+
## load using the default mapping
|
1311
|
+
p = Person.load_from_xml xml
|
1312
|
+
=> #<Person:0x007fb015506640 @name="Suzy", @age=28, @address=#<Address:0x007fb015505a38 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
|
1313
|
+
|
1314
|
+
## save using the default mapping
|
1315
|
+
xml2 = p.save_to_xml
|
1316
|
+
xml2.write $stdout,2
|
1317
|
+
<person name='Suzy' age='28'>
|
1318
|
+
<address>
|
1319
|
+
<street>
|
1320
|
+
Abbey Road
|
1321
|
+
</street>
|
1322
|
+
<number>
|
1323
|
+
72
|
1324
|
+
</number>
|
1325
|
+
<city>
|
1326
|
+
London
|
1327
|
+
</city>
|
1328
|
+
<zip>
|
1329
|
+
18827
|
1330
|
+
</zip>
|
1331
|
+
</address>
|
1332
|
+
</person>
|
1333
|
+
## xml2 identical to xml
|
1334
|
+
|
1335
|
+
|
1336
|
+
## now, save the same person to XML using the :other mapping...
|
1337
|
+
other_xml = p.save_to_xml :mapping=>:other
|
1338
|
+
other_xml.write $stdout,2
|
1339
|
+
<individual>
|
1340
|
+
<name>
|
1341
|
+
Suzy
|
1342
|
+
</name>
|
1343
|
+
<age>
|
1344
|
+
28
|
1345
|
+
</age>
|
1346
|
+
<street-name number='72'>
|
1347
|
+
Abbey Road
|
1348
|
+
</street-name>
|
1349
|
+
<city-name zip-code='18827'>
|
1350
|
+
London
|
1351
|
+
</city-name>
|
1352
|
+
</individual>
|
1353
|
+
## load it again using the :other mapping
|
1354
|
+
p2 = Person.load_from_xml other_xml, :mapping=>:other
|
1355
|
+
=> #<Person:0x007fb0154bad30 @name="Suzy", @age=28, @address=#<Address:0x007fb0154b98e0 @street="Abbey Road", @number=72, @city="London", @zip=18827>>
|
1356
|
+
|
1357
|
+
## p2 identical to p
|
639
1358
|
|
640
1359
|
In this example, each of the two mappings contains nodes that map the
|
641
1360
|
same set of Ruby attributes (name, age and address). This is probably
|
@@ -652,11 +1371,11 @@ sub-ordinated class (+Address+). This is the case for all
|
|
652
1371
|
specify a different mapping for the sub-object(s) using the option
|
653
1372
|
:sub_mapping, e.g.
|
654
1373
|
|
655
|
-
|
1374
|
+
object_node :address, "address", :class=>Address, :sub_mapping=>:other
|
656
1375
|
|
657
1376
|
|
658
1377
|
|
659
|
-
|
1378
|
+
## Defining your own Node Types
|
660
1379
|
|
661
1380
|
It's easy to write additional node types and register them with the
|
662
1381
|
xml-mapping library (the following node types come with xml-mapping:
|
@@ -665,18 +1384,32 @@ xml-mapping library (the following node types come with xml-mapping:
|
|
665
1384
|
|
666
1385
|
I'll first show an example, then some more theoretical insight.
|
667
1386
|
|
668
|
-
|
1387
|
+
### Example
|
669
1388
|
|
670
1389
|
Let's say we want to extend the +Signature+ class from the example to
|
671
1390
|
include the time at which the signature was created. We want the new
|
672
1391
|
XML representation of such a signature to look like this:
|
673
1392
|
|
674
|
-
|
1393
|
+
<Signature>
|
1394
|
+
<Name>John Doe</Name>
|
1395
|
+
<Position>product manager</Position>
|
1396
|
+
<signed-on>
|
1397
|
+
<day>13</day>
|
1398
|
+
<month>2</month>
|
1399
|
+
<year>2005</year>
|
1400
|
+
</signed-on>
|
1401
|
+
</Signature>
|
675
1402
|
|
676
1403
|
(we only save year, month and day to make this example shorter), and
|
677
1404
|
the mapping class declaration to look like this:
|
678
1405
|
|
679
|
-
|
1406
|
+
class Signature
|
1407
|
+
include XML::Mapping
|
1408
|
+
|
1409
|
+
text_node :name, "Name"
|
1410
|
+
text_node :position, "Position", :default_value=>"Some Employee"
|
1411
|
+
time_node :signed_on, "signed-on", :default_value=>Time.now
|
1412
|
+
end
|
680
1413
|
|
681
1414
|
(i.e. a new "time_node" declaration was added).
|
682
1415
|
|
@@ -686,7 +1419,34 @@ class +Time+.
|
|
686
1419
|
|
687
1420
|
This node type can be defined with this piece of code:
|
688
1421
|
|
689
|
-
|
1422
|
+
require 'xml/mapping/base'
|
1423
|
+
|
1424
|
+
class TimeNode < XML::Mapping::SingleAttributeNode
|
1425
|
+
def initialize(*args)
|
1426
|
+
path,*args = super(*args)
|
1427
|
+
@y_path = XML::XXPath.new(path+"/year")
|
1428
|
+
@m_path = XML::XXPath.new(path+"/month")
|
1429
|
+
@d_path = XML::XXPath.new(path+"/day")
|
1430
|
+
args
|
1431
|
+
end
|
1432
|
+
|
1433
|
+
def extract_attr_value(xml)
|
1434
|
+
y,m,d = default_when_xpath_err{ [@y_path.first(xml).text.to_i,
|
1435
|
+
@m_path.first(xml).text.to_i,
|
1436
|
+
@d_path.first(xml).text.to_i]
|
1437
|
+
}
|
1438
|
+
Time.local(y,m,d)
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
def set_attr_value(xml, value)
|
1442
|
+
@y_path.first(xml,:ensure_created=>true).text = value.year
|
1443
|
+
@m_path.first(xml,:ensure_created=>true).text = value.month
|
1444
|
+
@d_path.first(xml,:ensure_created=>true).text = value.day
|
1445
|
+
end
|
1446
|
+
end
|
1447
|
+
|
1448
|
+
|
1449
|
+
XML::Mapping.add_node_class TimeNode
|
690
1450
|
|
691
1451
|
The last line registers the new node type with the xml-mapping
|
692
1452
|
library. The name of the node factory method ("time_node") is
|
@@ -798,7 +1558,7 @@ _xml_. If it was not, it is created (preferably at the end of _xml_'s
|
|
798
1558
|
list of sub-nodes), and returned. See below[aref:xpath] for a more
|
799
1559
|
detailed documentation of the XPath interpreter.
|
800
1560
|
|
801
|
-
|
1561
|
+
### Element order in created XML documents
|
802
1562
|
|
803
1563
|
As just said, XML::XXPath, when used to create new XML nodes,
|
804
1564
|
generally appends those nodes to the end of the list of subnodes of
|
@@ -821,7 +1581,7 @@ The following is a more systematic overview of the basic node
|
|
821
1581
|
types. The description is self-contained, so some information from the
|
822
1582
|
previous section will be repeated.
|
823
1583
|
|
824
|
-
|
1584
|
+
### Node Types Are Ruby Classes
|
825
1585
|
|
826
1586
|
A node type is implemented as a Ruby class derived from
|
827
1587
|
XML::Mapping::Node or one of its subclasses.
|
@@ -830,16 +1590,16 @@ The following node types (node classes) come with xml-mapping (they
|
|
830
1590
|
all live in the XML::Mapping namespace, which I've left out here for
|
831
1591
|
brevity):
|
832
1592
|
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
1593
|
+
Node
|
1594
|
+
+-SingleAttributeNode
|
1595
|
+
| +-SubObjectBaseNode
|
1596
|
+
| | +-ObjectNode
|
1597
|
+
| | +-ArrayNode
|
1598
|
+
| | +-HashNode
|
1599
|
+
| +-TextNode
|
1600
|
+
| +-NumericNode
|
1601
|
+
| +-BooleanNode
|
1602
|
+
+-ChoiceNode
|
843
1603
|
|
844
1604
|
XML::Mapping::Node is the base class for all nodes,
|
845
1605
|
XML::Mapping::SingleAttributeNode is the base class for
|
@@ -863,12 +1623,12 @@ built-in nodes is not very long or complicated; you may consider
|
|
863
1623
|
reading it in addition to this text to gain a better understanding.
|
864
1624
|
|
865
1625
|
|
866
|
-
|
1626
|
+
### How Node Types Work
|
867
1627
|
|
868
1628
|
The xml-mapping core "operates" node types as follows:
|
869
1629
|
|
870
1630
|
|
871
|
-
|
1631
|
+
#### Node Initialization
|
872
1632
|
|
873
1633
|
As said above, when a node class is registered with xml-mapping by
|
874
1634
|
calling <tt>XML::Mapping.add_node_class TheNodeClass</tt>, xml-mapping
|
@@ -881,11 +1641,11 @@ the node class. The list of parameters to _new_ will consist of <i>the
|
|
881
1641
|
mapping class, followed by all arguments that were passed to the node
|
882
1642
|
factory method</i>. For example, when you have this node declaration:
|
883
1643
|
|
884
|
-
|
885
|
-
|
1644
|
+
class MyMappingClass
|
1645
|
+
include XML::Mapping
|
886
1646
|
|
887
|
-
|
888
|
-
|
1647
|
+
my_node :foo, "bar", 42, :hi=>"ho"
|
1648
|
+
end
|
889
1649
|
|
890
1650
|
, then the node factory method (+my_node+) calls
|
891
1651
|
<tt>MyNode.new(MyMappingClass, :foo, "bar", 42, :hi=>"ho")</tt>.
|
@@ -898,14 +1658,14 @@ processes itself, process them, and return an array containing the
|
|
898
1658
|
remaining (still unprocessed) parameters. Thus, an implementation of
|
899
1659
|
_initialize_ follows this pattern:
|
900
1660
|
|
901
|
-
|
902
|
-
|
1661
|
+
def initialize(*args)
|
1662
|
+
myparam1,myparam2,...,myparamx,*args = super(*args)
|
903
1663
|
|
904
|
-
|
1664
|
+
.... process the myparam1,myparam2,...,myparamx ....
|
905
1665
|
|
906
|
-
|
907
|
-
|
908
|
-
|
1666
|
+
# return still unprocessed args
|
1667
|
+
args
|
1668
|
+
end
|
909
1669
|
|
910
1670
|
(since the called superclass initializer is written the same way, the
|
911
1671
|
parameter array returned by it will already be stripped of all
|
@@ -928,7 +1688,7 @@ per node definition, and that instance lives in the mapping class the
|
|
928
1688
|
node was defined in.
|
929
1689
|
|
930
1690
|
|
931
|
-
|
1691
|
+
#### Node Operation during Marshalling and Unmarshalling
|
932
1692
|
|
933
1693
|
When an instance of a mapping class is created or filled from an XML
|
934
1694
|
tree, xml-mapping will call +xml_to_obj+ on all nodes defined in that
|
@@ -951,7 +1711,7 @@ the instance and put it into the appropriate XML elements/XML attr
|
|
951
1711
|
etc. of the XML tree.
|
952
1712
|
|
953
1713
|
|
954
|
-
|
1714
|
+
### Basic Node Types Overview
|
955
1715
|
|
956
1716
|
The following is an overview of how initialization and
|
957
1717
|
marshalling/unmarshalling is implemented in the node base classes
|
@@ -959,7 +1719,7 @@ marshalling/unmarshalling is implemented in the node base classes
|
|
959
1719
|
|
960
1720
|
TODO: summary table: member var name; introduced in class; meaning
|
961
1721
|
|
962
|
-
|
1722
|
+
#### Node
|
963
1723
|
|
964
1724
|
In _initialize_, the mapping class and the option arguments are
|
965
1725
|
stripped from the argument list. The mapping class is stored in
|
@@ -990,7 +1750,7 @@ The marshalling/unmarshalling methods
|
|
990
1750
|
+Node+ (they just raise an exception).
|
991
1751
|
|
992
1752
|
|
993
|
-
|
1753
|
+
#### SingleAttributeNode
|
994
1754
|
|
995
1755
|
In _initialize_, the attribute name is stripped from the argument list
|
996
1756
|
and stored in @attrname, and an attribute of that name is added to the
|
@@ -1003,9 +1763,9 @@ this, the <tt>obj_to_xml</tt>/<tt>xml_to_obj</tt> implementations in
|
|
1003
1763
|
SingleAttributeNode call two new methods introduced by
|
1004
1764
|
SingleAttributeNode, which must be overwritten by subclasses:
|
1005
1765
|
|
1006
|
-
|
1766
|
+
extract_attr_value(xml)
|
1007
1767
|
|
1008
|
-
|
1768
|
+
set_attr_value(xml, value)
|
1009
1769
|
|
1010
1770
|
<tt>extract_attr_value(xml)</tt> is called by <tt>xml_to_obj</tt>
|
1011
1771
|
during unmarshalling. _xml_ is the XML tree being read. The method
|
@@ -1028,7 +1788,7 @@ XML. SingleAttributeNode will catch this exception and put the default
|
|
1028
1788
|
value, if it was defined, into the attribute.
|
1029
1789
|
|
1030
1790
|
|
1031
|
-
|
1791
|
+
#### SubObjectBaseNode
|
1032
1792
|
|
1033
1793
|
The initializer will set up additional member variables @sub_mapping,
|
1034
1794
|
@marshaller, and @unmarshaller.
|
@@ -1045,7 +1805,7 @@ procs are there to be called from <tt>extract_attr_value</tt> or
|
|
1045
1805
|
<tt>set_attr_value</tt> whenever the need arises.
|
1046
1806
|
|
1047
1807
|
|
1048
|
-
|
1808
|
+
## XPath Interpreter
|
1049
1809
|
|
1050
1810
|
XML::XXPath is an XPath parser. It is used in xml-mapping node type
|
1051
1811
|
definitions, but can just as well be utilized stand-alone (it does not
|
@@ -1053,32 +1813,34 @@ depend on xml-mapping). XML::XXPath is very incomplete and probably
|
|
1053
1813
|
will always be, but it should be reasonably efficient (XPath
|
1054
1814
|
expressions are precompiled), and, most importantly, it supports write
|
1055
1815
|
access, which is needed for writing objects to XML. For example, if
|
1056
|
-
you create the path
|
1816
|
+
you create the path <tt>/foo/bar[3]/baz[@key='hiho']</tt> in the XML
|
1817
|
+
document
|
1057
1818
|
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1819
|
+
<foo>
|
1820
|
+
<bar>
|
1821
|
+
<baz key="ab">hello</baz>
|
1822
|
+
<baz key="xy">goodbye</baz>
|
1823
|
+
</bar>
|
1824
|
+
</foo>
|
1064
1825
|
|
1065
1826
|
, you'll get:
|
1066
1827
|
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1828
|
+
<foo>
|
1829
|
+
<bar>
|
1830
|
+
<baz key='ab'>hello</baz>
|
1831
|
+
<baz key='xy'>goodbye</baz>
|
1832
|
+
</bar>
|
1833
|
+
<bar/>
|
1834
|
+
<bar>
|
1835
|
+
<baz key='hiho'/>
|
1836
|
+
</bar>
|
1837
|
+
</foo>
|
1077
1838
|
|
1078
1839
|
XML::XXPath is explained in more detail in the reference documentation
|
1079
|
-
and the
|
1840
|
+
and the user_manual_xxpath file.
|
1080
1841
|
|
1081
1842
|
|
1082
|
-
|
1843
|
+
## License
|
1083
1844
|
|
1084
|
-
|
1845
|
+
xml-mapping is licensed under the Apache License, version 2.0. See the
|
1846
|
+
LICENSE file for details.
|