xml-mapping 0.8.1 → 0.9.1
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/ChangeLog +64 -3
- data/README +871 -173
- data/README_XPATH +40 -13
- data/Rakefile +37 -26
- data/TODO.txt +39 -8
- data/examples/README +5 -0
- data/examples/company_usage.intout +34 -22
- data/examples/documents_folders.rb +31 -0
- data/examples/documents_folders.xml +16 -0
- data/examples/documents_folders_usage.intin.rb +18 -0
- data/examples/documents_folders_usage.intout +46 -0
- data/examples/order_signature_enhanced_usage.intout +21 -11
- data/examples/order_usage.intin.rb +52 -5
- data/examples/order_usage.intout +154 -80
- data/examples/person.intin.rb +44 -0
- data/examples/person.intout +27 -0
- data/examples/person_mm.intin.rb +119 -0
- data/examples/person_mm.intout +114 -0
- data/examples/publication.intin.rb +44 -0
- data/examples/publication.intout +20 -0
- data/examples/reader.intin.rb +33 -0
- data/examples/reader.intout +19 -0
- data/examples/stringarray.rb +5 -0
- data/examples/stringarray.xml +10 -0
- data/examples/stringarray_usage.intin.rb +11 -0
- data/examples/stringarray_usage.intout +31 -0
- data/examples/time_augm.intout +19 -7
- data/examples/time_augm_loading.intin.rb +44 -0
- data/examples/time_augm_loading.intout +12 -0
- data/examples/time_node.intin.rb +79 -0
- data/examples/time_node.rb +3 -2
- data/examples/time_node_w_marshallers.intin.rb +48 -0
- data/examples/time_node_w_marshallers.intout +25 -0
- data/examples/time_node_w_marshallers.xml +9 -0
- data/examples/xpath_create_new.intout +132 -114
- data/examples/xpath_ensure_created.intout +86 -65
- data/examples/xpath_pathological.intout +16 -16
- data/examples/xpath_usage.intout +1 -1
- data/install.rb +1 -0
- data/lib/xml/mapping.rb +3 -1
- data/lib/xml/mapping/base.rb +442 -272
- data/lib/xml/mapping/core_classes_mapping.rb +32 -0
- data/lib/xml/mapping/standard_nodes.rb +176 -86
- data/lib/xml/mapping/version.rb +2 -2
- data/lib/xml/rexml_ext.rb +186 -0
- data/lib/xml/xxpath.rb +28 -265
- data/lib/xml/xxpath/steps.rb +345 -0
- data/lib/xml/xxpath_methods.rb +96 -0
- data/test/all_tests.rb +4 -1
- data/test/benchmark_fixtures.rb +14 -0
- data/test/{multiple_mappings.rb → bookmarks.rb} +0 -0
- data/test/company.rb +47 -0
- data/test/documents_folders.rb +11 -1
- data/test/examples_test.rb +29 -0
- data/test/fixtures/benchmark.xml +77 -0
- data/test/fixtures/company1.xml +9 -0
- data/test/fixtures/documents_folders.xml +0 -8
- data/test/fixtures/documents_folders2.xml +13 -19
- data/test/fixtures/triangle_m1.xml +17 -0
- data/test/fixtures/triangle_m2.xml +19 -0
- data/test/inheritance_test.rb +50 -0
- data/test/multiple_mappings_test.rb +155 -0
- data/test/rexml_xpath_benchmark.rb +29 -0
- data/test/triangle_mm.rb +57 -0
- data/test/xml_mapping_adv_test.rb +36 -1
- data/test/xml_mapping_test.rb +136 -7
- data/test/xpath_test.rb +154 -0
- data/test/xxpath_benchmark.rb +36 -0
- data/test/xxpath_benchmark.result1.txt +17 -0
- data/test/xxpath_methods_test.rb +61 -0
- metadata +139 -90
@@ -17,98 +17,119 @@ rootelt=d.root
|
|
17
17
|
XML::XXPath.new("/bar/baz[@key='work']").first(rootelt,:ensure_created=>true)
|
18
18
|
=> <baz key='work'> ... </>
|
19
19
|
d.write($stdout,2)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
|
21
|
+
<foo>
|
22
|
+
<bar>
|
23
|
+
<baz key='work'>
|
24
|
+
Java
|
25
|
+
</baz>
|
26
|
+
<baz key='play'>
|
27
|
+
Ruby
|
28
|
+
</baz>
|
29
|
+
</bar>
|
30
|
+
</foo>### no change (path existed before)
|
28
31
|
|
29
32
|
|
30
33
|
XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
|
31
34
|
=> <baz key='42'/>
|
32
35
|
d.write($stdout,2)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
|
37
|
+
<foo>
|
38
|
+
<bar>
|
39
|
+
<baz key='work'>
|
40
|
+
Java
|
41
|
+
</baz>
|
42
|
+
<baz key='play'>
|
43
|
+
Ruby
|
44
|
+
</baz>
|
37
45
|
<baz key='42'/>
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
### path was added
|
46
|
+
</bar>
|
47
|
+
</foo>### path was added
|
42
48
|
|
43
49
|
XML::XXPath.new("/bar/baz[@key='42']").first(rootelt,:ensure_created=>true)
|
44
50
|
=> <baz key='42'/>
|
45
51
|
d.write($stdout,2)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
|
53
|
+
<foo>
|
54
|
+
<bar>
|
55
|
+
<baz key='work'>
|
56
|
+
Java
|
57
|
+
</baz>
|
58
|
+
<baz key='play'>
|
59
|
+
Ruby
|
60
|
+
</baz>
|
50
61
|
<baz key='42'/>
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
### no change this time
|
62
|
+
</bar>
|
63
|
+
</foo>### no change this time
|
55
64
|
|
56
65
|
XML::XXPath.new("/bar/baz[@key2='hello']").first(rootelt,:ensure_created=>true)
|
57
|
-
=> <baz
|
66
|
+
=> <baz key='work' key2='hello'> ... </>
|
58
67
|
d.write($stdout,2)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
68
|
+
|
69
|
+
<foo>
|
70
|
+
<bar>
|
71
|
+
<baz key='work' key2='hello'>
|
72
|
+
Java
|
73
|
+
</baz>
|
74
|
+
<baz key='play'>
|
75
|
+
Ruby
|
76
|
+
</baz>
|
63
77
|
<baz key='42'/>
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
### this fit in the 1st "baz" element since
|
78
|
+
</bar>
|
79
|
+
</foo>### this fit in the 1st "baz" element since
|
68
80
|
### there was no "key2" attribute there before.
|
69
81
|
|
70
82
|
XML::XXPath.new("/bar/baz[2]").first(rootelt,:ensure_created=>true)
|
71
83
|
=> <baz key='play'> ... </>
|
72
84
|
d.write($stdout,2)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
85
|
+
|
86
|
+
<foo>
|
87
|
+
<bar>
|
88
|
+
<baz key='work' key2='hello'>
|
89
|
+
Java
|
90
|
+
</baz>
|
91
|
+
<baz key='play'>
|
92
|
+
Ruby
|
93
|
+
</baz>
|
77
94
|
<baz key='42'/>
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
### no change
|
95
|
+
</bar>
|
96
|
+
</foo>### no change
|
82
97
|
|
83
98
|
XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
|
84
|
-
=> #<XML::XXPath::Accessors::Attribute:
|
99
|
+
=> #<XML::XXPath::Accessors::Attribute:0x7f4876522658 @name="haha", @parent=<baz haha='[unset]'/>>
|
85
100
|
d.write($stdout,2)
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
101
|
+
|
102
|
+
<foo>
|
103
|
+
<bar>
|
104
|
+
<baz key='work' key2='hello'>
|
105
|
+
Java
|
106
|
+
</baz>
|
107
|
+
<baz key='play'>
|
108
|
+
Ruby
|
109
|
+
</baz>
|
90
110
|
<baz key='42'/>
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
|
111
|
+
<baz/>
|
112
|
+
<baz/>
|
113
|
+
<baz haha='[unset]'/>
|
114
|
+
</bar>
|
115
|
+
</foo>### for there to be a 6th "baz" element, there must be 1st..5th "baz" elements
|
98
116
|
|
99
117
|
XML::XXPath.new("/bar/baz[6]/@haha").first(rootelt,:ensure_created=>true)
|
100
|
-
=> #<XML::XXPath::Accessors::Attribute:
|
118
|
+
=> #<XML::XXPath::Accessors::Attribute:0x7f4876516bc8 @name="haha", @parent=<baz haha='[unset]'/>>
|
101
119
|
d.write($stdout,2)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
120
|
+
|
121
|
+
<foo>
|
122
|
+
<bar>
|
123
|
+
<baz key='work' key2='hello'>
|
124
|
+
Java
|
125
|
+
</baz>
|
126
|
+
<baz key='play'>
|
127
|
+
Ruby
|
128
|
+
</baz>
|
106
129
|
<baz key='42'/>
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
### no change this time
|
130
|
+
<baz/>
|
131
|
+
<baz/>
|
132
|
+
<baz haha='[unset]'/>
|
133
|
+
</bar>
|
134
|
+
</foo>### no change this time
|
114
135
|
|
@@ -24,14 +24,13 @@ newelt = XML::XXPath.new("bar/*").first(rootelt, :ensure_created=>true)
|
|
24
24
|
=> </>
|
25
25
|
|
26
26
|
d.write($stdout,2)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
|
28
|
+
<foo>
|
29
|
+
<bar>
|
30
|
+
</>
|
31
|
+
</bar>
|
32
|
+
<bar/>
|
33
|
+
</foo>
|
35
34
|
### a new "unspecified" element was created
|
36
35
|
newelt.unspecified?
|
37
36
|
=> true
|
@@ -43,14 +42,15 @@ newelt.unspecified?
|
|
43
42
|
=> false
|
44
43
|
|
45
44
|
d.write($stdout,2)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
45
|
+
|
46
|
+
<foo>
|
47
|
+
<bar>
|
48
|
+
<new-one>
|
49
|
+
hello!
|
50
|
+
</new-one>
|
51
|
+
</bar>
|
52
|
+
<bar/>
|
53
|
+
</foo>
|
54
54
|
### you could also set unspecified to false explicitly, as in:
|
55
55
|
newelt.unspecified=true
|
56
56
|
|
data/examples/xpath_usage.intout
CHANGED
@@ -44,7 +44,7 @@ path2.all(d)
|
|
44
44
|
## "first" raises XML::XXPathError in such cases...
|
45
45
|
path2.first(d)
|
46
46
|
XML::XXPathError: path not found: /foo/bar[2]/baz[4]
|
47
|
-
from ../lib/xml
|
47
|
+
from ../lib/xml/xxpath.rb:75:in `first'
|
48
48
|
|
49
49
|
##...unless we allow nil returns
|
50
50
|
path2.first(d,:allow_nil=>true)
|
data/install.rb
CHANGED
data/lib/xml/mapping.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# xml-mapping -- bidirectional Ruby-XML mapper
|
2
|
-
# Copyright (C) 2004
|
2
|
+
# Copyright (C) 2004-2006 Olaf Klischat
|
3
3
|
|
4
4
|
$:.unshift(File.dirname(__FILE__)+"/..")
|
5
5
|
|
6
6
|
require 'xml/mapping/base'
|
7
7
|
require 'xml/mapping/standard_nodes'
|
8
8
|
|
9
|
+
XML::Mapping.add_node_class XML::Mapping::Node
|
9
10
|
XML::Mapping.add_node_class XML::Mapping::TextNode
|
10
11
|
XML::Mapping.add_node_class XML::Mapping::NumericNode
|
11
12
|
XML::Mapping.add_node_class XML::Mapping::ObjectNode
|
12
13
|
XML::Mapping.add_node_class XML::Mapping::BooleanNode
|
13
14
|
XML::Mapping.add_node_class XML::Mapping::ArrayNode
|
14
15
|
XML::Mapping.add_node_class XML::Mapping::HashNode
|
16
|
+
XML::Mapping.add_node_class XML::Mapping::ChoiceNode
|
data/lib/xml/mapping/base.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# xml-mapping -- bidirectional Ruby-XML mapper
|
2
|
-
# Copyright (C) 2004
|
2
|
+
# Copyright (C) 2004-2006 Olaf Klischat
|
3
3
|
|
4
4
|
require 'rexml/document'
|
5
5
|
require "xml/xxpath"
|
@@ -60,6 +60,14 @@ module XML
|
|
60
60
|
# Including XML::Mapping also adds all methods of
|
61
61
|
# XML::Mapping::ClassMethods to your class (as class methods).
|
62
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
|
+
#
|
63
71
|
# As you may have noticed from the example, the node factory methods
|
64
72
|
# generally use XPath expressions to specify locations in the mapped
|
65
73
|
# XML document. To make this work, XML::Mapping relies on
|
@@ -70,45 +78,96 @@ module XML
|
|
70
78
|
# elements/documents in memory.
|
71
79
|
module Mapping
|
72
80
|
|
73
|
-
#
|
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
|
74
86
|
# shared by all class methods mixed into classes by including
|
75
87
|
# Mapping. See
|
76
88
|
# http://user.cs.tu-berlin.de/~klischat/mydocs/ruby/mixin_class_methods_global_state.txt.html
|
77
89
|
# for a more detailed discussion.
|
78
|
-
|
79
|
-
|
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
|
+
|
80
107
|
|
81
108
|
def self.append_features(base) #:nodoc:
|
82
109
|
super
|
83
110
|
base.extend(ClassMethods)
|
84
|
-
|
111
|
+
Classes_by_rootelt_names.create_classes_for(base.default_root_element_name, :_default) << base
|
112
|
+
base.initializing_xml_mapping
|
85
113
|
end
|
86
114
|
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
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
|
90
120
|
# <class>.root_element_name (see
|
91
121
|
# XML::Mapping::ClassMethods.root_element_name).
|
92
|
-
def self.class_for_root_elt_name(name)
|
122
|
+
def self.class_for_root_elt_name(name, options={:mapping=>:_default})
|
93
123
|
# TODO: implement Hash read-only instead of this
|
94
124
|
# interface
|
95
|
-
|
96
|
-
Classes_w_default_rootelt_names[name]
|
125
|
+
Classes_by_rootelt_names.classes_for(name, options[:mapping])[-1]
|
97
126
|
end
|
98
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
|
99
138
|
|
100
|
-
|
101
|
-
|
102
|
-
|
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])
|
103
162
|
end
|
104
163
|
end
|
105
164
|
|
106
|
-
#
|
107
|
-
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# inherit this behaviour.
|
165
|
+
# Initializer. Called (by Class#new) after _self_ was created
|
166
|
+
# using _new_.
|
167
|
+
#
|
168
|
+
# XML::Mapping's implementation calls #initialize_xml_mapping.
|
111
169
|
def initialize(*args)
|
170
|
+
super(*args)
|
112
171
|
initialize_xml_mapping
|
113
172
|
end
|
114
173
|
|
@@ -119,19 +178,21 @@ module XML
|
|
119
178
|
# object's class are processed (i.e. have their
|
120
179
|
# #xml_to_obj method called) in the order of their definition
|
121
180
|
# inside the class, then #post_load is called.
|
122
|
-
def fill_from_xml(xml)
|
123
|
-
|
124
|
-
|
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|
|
125
186
|
node.xml_to_obj self, xml
|
126
187
|
end
|
127
|
-
post_load
|
188
|
+
post_load :mapping=>options[:mapping]
|
128
189
|
end
|
129
190
|
|
130
191
|
# This method is called immediately before _self_ is filled from
|
131
192
|
# an xml source. _xml_ is the source REXML::Element.
|
132
193
|
#
|
133
194
|
# The default implementation of this method is empty.
|
134
|
-
def pre_load(xml)
|
195
|
+
def pre_load(xml, options={:mapping=>:_default})
|
135
196
|
end
|
136
197
|
|
137
198
|
|
@@ -143,7 +204,7 @@ module XML
|
|
143
204
|
# exception to abandon the whole loading process.
|
144
205
|
#
|
145
206
|
# The default implementation of this method is empty.
|
146
|
-
def post_load
|
207
|
+
def post_load(options={:mapping=>:_default})
|
147
208
|
end
|
148
209
|
|
149
210
|
|
@@ -152,8 +213,8 @@ module XML
|
|
152
213
|
# (i.e. have their
|
153
214
|
# #obj_to_xml method called) in the order of their definition
|
154
215
|
# inside the class.
|
155
|
-
def fill_into_xml(xml)
|
156
|
-
self.class.all_xml_mapping_nodes.each do |node|
|
216
|
+
def fill_into_xml(xml, options={:mapping=>:_default})
|
217
|
+
self.class.all_xml_mapping_nodes(:mapping=>options[:mapping]).each do |node|
|
157
218
|
node.obj_to_xml self,xml
|
158
219
|
end
|
159
220
|
end
|
@@ -163,10 +224,10 @@ module XML
|
|
163
224
|
#
|
164
225
|
# This method calls #pre_save, then #fill_into_xml, then
|
165
226
|
# #post_save.
|
166
|
-
def save_to_xml
|
167
|
-
xml = pre_save
|
168
|
-
fill_into_xml
|
169
|
-
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]
|
170
231
|
xml
|
171
232
|
end
|
172
233
|
|
@@ -181,47 +242,271 @@ module XML
|
|
181
242
|
# class name, with capital letters converted to lowercase and
|
182
243
|
# preceded by a dash, e.g. "MySampleClass" becomes
|
183
244
|
# "my-sample-class".
|
184
|
-
def pre_save
|
185
|
-
REXML::Element.new(self.class.root_element_name)
|
245
|
+
def pre_save(options={:mapping=>:_default})
|
246
|
+
REXML::Element.new(self.class.root_element_name(:mapping=>options[:mapping]))
|
186
247
|
end
|
187
248
|
|
188
249
|
# This method is called immediately after _self_'s state has been
|
189
250
|
# filled into an XML element.
|
190
251
|
#
|
191
252
|
# The default implementation does nothing.
|
192
|
-
def post_save(xml)
|
253
|
+
def post_save(xml, options={:mapping=>:_default})
|
193
254
|
end
|
194
255
|
|
195
256
|
|
196
257
|
# Save _self_'s state as XML into the file named _filename_.
|
197
258
|
# The XML is obtained by calling #save_to_xml.
|
198
|
-
def save_to_file(filename)
|
199
|
-
xml = save_to_xml
|
259
|
+
def save_to_file(filename, options={:mapping=>:_default})
|
260
|
+
xml = save_to_xml :mapping=>options[:mapping]
|
200
261
|
File.open(filename,"w") do |f|
|
201
|
-
|
262
|
+
REXML::Formatters::Transitive.new(2,false).write(xml,f)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
# The instance methods of this module are automatically added as
|
268
|
+
# class methods to a class that includes XML::Mapping.
|
269
|
+
module ClassMethods
|
270
|
+
#ClassMethods = Module.new do # this is the alternative -- but see above for peculiarities
|
271
|
+
|
272
|
+
# all nodes of this class, in the order of their definition,
|
273
|
+
# hashed by mapping (hash mapping => array of nodes)
|
274
|
+
def xml_mapping_nodes_hash #:nodoc:
|
275
|
+
@xml_mapping_nodes ||= {}
|
276
|
+
end
|
277
|
+
|
278
|
+
# called on a class when it is being made a mapping class
|
279
|
+
# (i.e. immediately after XML::Mapping was included in it)
|
280
|
+
def initializing_xml_mapping #:nodoc:
|
281
|
+
@default_mapping = :_default
|
282
|
+
end
|
283
|
+
|
284
|
+
# Make _mapping_ the mapping to be used by default in future
|
285
|
+
# node declarations in this class. The default can be
|
286
|
+
# overwritten on a per-node basis by passing a :mapping option
|
287
|
+
# parameter to the node factory method
|
288
|
+
#
|
289
|
+
# The initial default mapping in a mapping class is :_default
|
290
|
+
def use_mapping mapping
|
291
|
+
@default_mapping = mapping
|
292
|
+
xml_mapping_nodes_hash[mapping] ||= [] # create empty mapping node list if
|
293
|
+
# there wasn't one before so future calls
|
294
|
+
# to load/save_xml etc. w/ this mapping don't raise
|
295
|
+
end
|
296
|
+
|
297
|
+
# return the current default mapping (:_default initially, or
|
298
|
+
# the value set with the latest call to use_mapping)
|
299
|
+
def default_mapping
|
300
|
+
@default_mapping
|
301
|
+
end
|
302
|
+
|
303
|
+
# Add getter and setter methods for a new attribute named _name_
|
304
|
+
# to this class. This is a convenience method intended to be
|
305
|
+
# called from Node class initializers.
|
306
|
+
def add_accessor(name)
|
307
|
+
name = name.id2name if name.kind_of? Symbol
|
308
|
+
unless self.instance_methods.include?(name)
|
309
|
+
self.module_eval <<-EOS
|
310
|
+
attr_reader :#{name}
|
311
|
+
EOS
|
312
|
+
end
|
313
|
+
unless self.instance_methods.include?("#{name}=")
|
314
|
+
self.module_eval <<-EOS
|
315
|
+
attr_writer :#{name}
|
316
|
+
EOS
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Create a new instance of this class from the XML contained in
|
321
|
+
# the file named _filename_. Calls load_from_xml internally.
|
322
|
+
def load_from_file(filename, options={:mapping=>:_default})
|
323
|
+
xml = REXML::Document.new(File.new(filename))
|
324
|
+
load_from_xml xml.root, :mapping=>options[:mapping]
|
325
|
+
end
|
326
|
+
|
327
|
+
# Create a new instance of this class from the XML contained in
|
328
|
+
# _xml_ (a REXML::Element).
|
329
|
+
#
|
330
|
+
# Allocates a new object, then calls fill_from_xml(_xml_) on
|
331
|
+
# it.
|
332
|
+
def load_from_xml(xml, options={:mapping=>:_default})
|
333
|
+
raise(MappingError, "undefined mapping: #{options[:mapping].inspect}") \
|
334
|
+
unless xml_mapping_nodes_hash.has_key?(options[:mapping])
|
335
|
+
# create the new object. It is recommended that the class
|
336
|
+
# have a no-argument initializer, so try new first. If that
|
337
|
+
# doesn't work, try allocate, which bypasses the initializer.
|
338
|
+
begin
|
339
|
+
obj = self.new
|
340
|
+
rescue ArgumentError # TODO: this may hide real errors.
|
341
|
+
# how to statically check whether
|
342
|
+
# self self.new accepts an empty
|
343
|
+
# argument list?
|
344
|
+
obj = self.allocate
|
345
|
+
end
|
346
|
+
obj.initialize_xml_mapping :mapping=>options[:mapping]
|
347
|
+
obj.fill_from_xml xml, :mapping=>options[:mapping]
|
348
|
+
obj
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# array of all nodes defined in this class, in the order of
|
353
|
+
# their definition. Option :create specifies whether or not an
|
354
|
+
# empty array should be created and returned if there was none
|
355
|
+
# before (if not, an exception is raised). :mapping specifies
|
356
|
+
# the mapping the returned nodes must have been defined in; nil
|
357
|
+
# means return all nodes regardless of their mapping
|
358
|
+
def xml_mapping_nodes(options={:mapping=>nil,:create=>true})
|
359
|
+
unless options[:mapping]
|
360
|
+
return xml_mapping_nodes_hash.values.inject([]){|a1,a2|a1+a2}
|
361
|
+
end
|
362
|
+
options[:create] = true if options[:create].nil?
|
363
|
+
if options[:create]
|
364
|
+
xml_mapping_nodes_hash[options[:mapping]] ||= []
|
365
|
+
else
|
366
|
+
xml_mapping_nodes_hash[options[:mapping]] ||
|
367
|
+
raise(MappingError, "undefined mapping: #{options[:mapping].inspect}")
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
# enumeration of all nodes in effect when
|
373
|
+
# marshalling/unmarshalling this class, that is, nodes defined
|
374
|
+
# for this class as well as for its superclasses. The nodes are
|
375
|
+
# returned in the order of their definition, starting with the
|
376
|
+
# topmost superclass that has nodes defined. keyword arguments
|
377
|
+
# are the same as for #xml_mapping_nodes.
|
378
|
+
def all_xml_mapping_nodes(options={:mapping=>nil,:create=>true})
|
379
|
+
# TODO: we could return a dynamic Enumerable here, or cache
|
380
|
+
# the array...
|
381
|
+
result = []
|
382
|
+
if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
|
383
|
+
result += superclass.all_xml_mapping_nodes options
|
384
|
+
end
|
385
|
+
result += xml_mapping_nodes options
|
202
386
|
end
|
387
|
+
|
388
|
+
|
389
|
+
# The "root element name" of this class (combined getter/setter
|
390
|
+
# method).
|
391
|
+
#
|
392
|
+
# The root element name is the name of the root element of the
|
393
|
+
# XML tree returned by <this class>.#save_to_xml (or, more
|
394
|
+
# specifically, <this class>.#pre_save). By default, this method
|
395
|
+
# returns the #default_root_element_name; you may call this
|
396
|
+
# method with an argument to set the root element name to
|
397
|
+
# something other than the default. The option argument :mapping
|
398
|
+
# specifies the mapping the root element is/will be defined in,
|
399
|
+
# it defaults to the current default mapping (:_default
|
400
|
+
# initially, or the value set with the latest call to
|
401
|
+
# use_mapping)
|
402
|
+
def root_element_name(name=nil, options={:mapping=>@default_mapping})
|
403
|
+
if Hash===name # ugly...
|
404
|
+
options=name; name=nil
|
405
|
+
end
|
406
|
+
@root_element_names ||= {}
|
407
|
+
if name
|
408
|
+
Classes_by_rootelt_names.remove_class root_element_name, options[:mapping], self
|
409
|
+
@root_element_names[options[:mapping]] = name
|
410
|
+
Classes_by_rootelt_names.create_classes_for(name, options[:mapping]) << self
|
411
|
+
end
|
412
|
+
@root_element_names[options[:mapping]] || default_root_element_name
|
413
|
+
end
|
414
|
+
|
415
|
+
# The default root element name for this class. Equals the class
|
416
|
+
# name, with all parent module names stripped, and with capital
|
417
|
+
# letters converted to lowercase and preceded by a dash;
|
418
|
+
# e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
|
419
|
+
def default_root_element_name
|
420
|
+
self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
|
421
|
+
end
|
422
|
+
|
203
423
|
end
|
204
424
|
|
205
425
|
|
426
|
+
|
427
|
+
# "polymorphic" load function. Turns the XML tree _xml_ into an
|
428
|
+
# object, which is returned. The class of the object and the
|
429
|
+
# mapping to be used for unmarshalling are automatically
|
430
|
+
# determined from the root element name of _xml_ using
|
431
|
+
# XML::Mapping.class_for_root_elt_name. If :mapping is non-nil,
|
432
|
+
# only root element names defined in that mapping will be
|
433
|
+
# considered (default is to consider all classes)
|
434
|
+
def self.load_object_from_xml(xml,options={:mapping=>nil})
|
435
|
+
if mapping = options[:mapping]
|
436
|
+
c = class_for_root_elt_name xml.name, :mapping=>mapping
|
437
|
+
else
|
438
|
+
c,mapping = class_and_mapping_for_root_elt_name(xml.name)
|
439
|
+
end
|
440
|
+
unless c
|
441
|
+
raise MappingError, "no mapping class for root element name #{xml.name}, mapping #{mapping.inspect}"
|
442
|
+
end
|
443
|
+
c.load_from_xml xml, :mapping=>mapping
|
444
|
+
end
|
445
|
+
|
446
|
+
# Like load_object_from_xml, but loads from the XML file named by
|
447
|
+
# _filename_.
|
448
|
+
def self.load_object_from_file(filename,options={:mapping=>nil})
|
449
|
+
xml = REXML::Document.new(File.new(filename))
|
450
|
+
load_object_from_xml xml.root, options
|
451
|
+
end
|
452
|
+
|
453
|
+
|
454
|
+
# Registers the new node class _c_ (must be a descendant of Node)
|
455
|
+
# with the xml-mapping framework.
|
456
|
+
#
|
457
|
+
# A new "factory method" will automatically be added to
|
458
|
+
# ClassMethods (and therefore to all classes that include
|
459
|
+
# XML::Mapping from now on); so you can call it from the body of
|
460
|
+
# your mapping class definition in order to create nodes of type
|
461
|
+
# _c_. The name of the factory method is derived by "underscoring"
|
462
|
+
# the (unqualified) name of _c_;
|
463
|
+
# e.g. _c_==<tt>Foo::Bar::MyNiftyNode</tt> will result in the
|
464
|
+
# creation of a factory method named +my_nifty_node+. The
|
465
|
+
# generated factory method creates and returns a new instance of
|
466
|
+
# _c_. The list of argument to _c_.new consists of _self_
|
467
|
+
# (i.e. the mapping class the factory method was called from)
|
468
|
+
# followed by the arguments passed to the factory method. You
|
469
|
+
# should always use the factory methods to create instances of
|
470
|
+
# node classes; you should never need to call a node class's
|
471
|
+
# constructor directly.
|
472
|
+
#
|
473
|
+
# For a demonstration, see the calls to +text_node+, +array_node+
|
474
|
+
# etc. in the examples along with the corresponding node classes
|
475
|
+
# TextNode, ArrayNode etc. (these predefined node classes are in
|
476
|
+
# no way "special"; they're added using add_node_class in
|
477
|
+
# mapping.rb just like any custom node classes would be).
|
478
|
+
def self.add_node_class(c)
|
479
|
+
meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
|
480
|
+
ClassMethods.module_eval <<-EOS
|
481
|
+
def #{meth_name}(*args)
|
482
|
+
#{c.name}.new(self,*args)
|
483
|
+
end
|
484
|
+
EOS
|
485
|
+
end
|
486
|
+
|
487
|
+
|
488
|
+
###### core node classes
|
489
|
+
|
206
490
|
# Abstract base class for all node types. As mentioned in the
|
207
491
|
# documentation for XML::Mapping, node types must be registered
|
208
|
-
# using add_node_class, and a corresponding "node
|
209
|
-
# (e.g. "text_node") will then be added as a class
|
210
|
-
# mapping classes. The node factory method is
|
211
|
-
# of the mapping classes as demonstrated in
|
212
|
-
# creates an instance of its corresponding node
|
213
|
-
# parameters to the node factory method,
|
214
|
-
# mapping class, will be passed to the
|
215
|
-
# type) and adds it to its owning mapping
|
216
|
-
# node object per node definition per
|
217
|
-
# object will handle all XML
|
218
|
-
# node, for all instances of
|
219
|
-
# the
|
220
|
-
# instance (fill_into_xml
|
221
|
-
# will call obj_to_xml
|
222
|
-
#
|
223
|
-
# REXML element the data is to be
|
224
|
-
# as well as the object the data
|
492
|
+
# using XML::Mapping.add_node_class, and a corresponding "node
|
493
|
+
# factory method" (e.g. "text_node") will then be added as a class
|
494
|
+
# method to your mapping classes. The node factory method is
|
495
|
+
# called from the body of the mapping classes as demonstrated in
|
496
|
+
# the examples. It creates an instance of its corresponding node
|
497
|
+
# type (the list of parameters to the node factory method,
|
498
|
+
# preceded by the owning mapping class, will be passed to the
|
499
|
+
# constructor of the node type) and adds it to its owning mapping
|
500
|
+
# class, so there is one node object per node definition per
|
501
|
+
# mapping class. That node object will handle all XML
|
502
|
+
# marshalling/unmarshalling for this node, for all instances of
|
503
|
+
# the mapping class. For this purpose, the marshalling and
|
504
|
+
# unmarshalling methods of a mapping class instance (fill_into_xml
|
505
|
+
# and fill_from_xml, respectively) will call obj_to_xml
|
506
|
+
# resp. xml_to_obj on all nodes of the mapping class, in the order
|
507
|
+
# of their definition, passing the REXML element the data is to be
|
508
|
+
# marshalled to/unmarshalled from as well as the object the data
|
509
|
+
# is to be read from/filled into.
|
225
510
|
#
|
226
511
|
# Node types that map some XML data to a single attribute of their
|
227
512
|
# mapping class (that should be most of them) shouldn't be
|
@@ -230,14 +515,55 @@ module XML
|
|
230
515
|
class Node
|
231
516
|
# Intializer, to be called from descendant classes. _owner_ is
|
232
517
|
# the mapping class this node is being defined in. It'll be
|
233
|
-
# stored in _@owner_.
|
234
|
-
|
518
|
+
# stored in _@owner_. @options will be set to a (possibly empty)
|
519
|
+
# hash containing the option arguments passed to
|
520
|
+
# _initialize_. Options :mapping, :reader and :writer will be
|
521
|
+
# handled, subclasses may handle additional options. See the
|
522
|
+
# section on defining nodes in the README for details.
|
523
|
+
def initialize(owner,*args)
|
235
524
|
@owner = owner
|
236
|
-
|
525
|
+
if Hash===args[-1]
|
526
|
+
@options = args[-1]
|
527
|
+
args = args[0..-2]
|
528
|
+
else
|
529
|
+
@options={}
|
530
|
+
end
|
531
|
+
@mapping = @options[:mapping] || owner.default_mapping
|
532
|
+
owner.xml_mapping_nodes(:mapping=>@mapping) << self
|
533
|
+
XML::Mapping::Classes_by_rootelt_names.ensure_exists owner.root_element_name, @mapping, owner
|
534
|
+
if @options[:reader]
|
535
|
+
# override xml_to_obj in this instance with invocation of
|
536
|
+
# @options[:reader]
|
537
|
+
class << self
|
538
|
+
alias_method :default_xml_to_obj, :xml_to_obj
|
539
|
+
def xml_to_obj(obj,xml)
|
540
|
+
begin
|
541
|
+
@options[:reader].call(obj,xml)
|
542
|
+
rescue ArgumentError
|
543
|
+
@options[:reader].call(obj,xml,self.method(:default_xml_to_obj))
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
if @options[:writer]
|
549
|
+
# override obj_to_xml in this instance with invocation of
|
550
|
+
# @options[:writer]
|
551
|
+
class << self
|
552
|
+
alias_method :default_obj_to_xml, :obj_to_xml
|
553
|
+
def obj_to_xml(obj,xml)
|
554
|
+
begin
|
555
|
+
@options[:writer].call(obj,xml)
|
556
|
+
rescue ArgumentError
|
557
|
+
@options[:writer].call(obj,xml,self.method(:default_obj_to_xml))
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
args
|
237
563
|
end
|
238
564
|
# This is called by the XML unmarshalling machinery when the
|
239
565
|
# state of an instance of this node's @owner is to be read from
|
240
|
-
# an XML
|
566
|
+
# an XML tree. _obj_ is the instance, _xml_ is the tree (a
|
241
567
|
# REXML::Element). The node must read "its" data from _xml_
|
242
568
|
# (using XML::XXPath or any other means) and store it to the
|
243
569
|
# corresponding parts (attributes etc.) of _obj_'s state.
|
@@ -246,32 +572,44 @@ module XML
|
|
246
572
|
end
|
247
573
|
# This is called by the XML unmarshalling machinery when the
|
248
574
|
# state of an instance of this node's @owner is to be stored
|
249
|
-
# into an XML
|
250
|
-
#
|
251
|
-
#
|
575
|
+
# into an XML tree. _obj_ is the instance, _xml_ is the tree (a
|
576
|
+
# REXML::Element). The node must extract "its" data from _obj_
|
577
|
+
# and store it to the corresponding parts (sub-elements,
|
252
578
|
# attributes etc.) of _xml_ (using XML::XXPath or any other
|
253
579
|
# means).
|
254
580
|
def obj_to_xml(obj,xml)
|
255
581
|
raise "abstract method called"
|
256
582
|
end
|
257
|
-
# Called when a new instance
|
258
|
-
#
|
259
|
-
#
|
260
|
-
#
|
261
|
-
|
583
|
+
# Called when a new instance of the mapping class this node
|
584
|
+
# belongs to is being initialized. _obj_ is the
|
585
|
+
# instance. _mapping_ is the mapping the initialization is
|
586
|
+
# happening with, if any: If the instance is being initialized
|
587
|
+
# as part of e.g. <tt>Class.load_from_file(name,
|
588
|
+
# :mapping=>:some_mapping</tt> or any other call that specifies
|
589
|
+
# a mapping, that mapping will be passed to this method. If the
|
590
|
+
# instance is being initialized normally with
|
591
|
+
# <tt>Class.new</tt>, _mapping_ is nil here.
|
592
|
+
#
|
593
|
+
# You may set up initial values for the attributes this node is
|
594
|
+
# responsible for here. Default implementation is empty.
|
595
|
+
def obj_initializing(obj,mapping)
|
596
|
+
end
|
597
|
+
# tell whether this node's data is present in _obj_ (when this
|
598
|
+
# method is called, _obj_ will be an instance of the mapping
|
599
|
+
# class this node was defined in). This method is currently used
|
600
|
+
# only by ChoiceNode when writing data back to XML. See
|
601
|
+
# ChoiceNode#obj_to_xml.
|
602
|
+
def is_present_in? obj
|
603
|
+
true
|
262
604
|
end
|
263
605
|
end
|
264
606
|
|
265
607
|
|
266
608
|
# Base class for node types that map some XML data to a single
|
267
|
-
# attribute of their mapping class.
|
268
|
-
# general "options" hash parameter which may be used to influence
|
269
|
-
# the creation of nodes in numerous ways, e.g. by providing
|
270
|
-
# default attribute values when there is no source data in the
|
271
|
-
# mapped XML.
|
609
|
+
# attribute of their mapping class.
|
272
610
|
#
|
273
|
-
# All node types that come with xml-mapping
|
274
|
-
# SingleAttributeNode.
|
611
|
+
# All node types that come with xml-mapping except one
|
612
|
+
# (ChoiceNode) inherit from SingleAttributeNode.
|
275
613
|
class SingleAttributeNode < Node
|
276
614
|
# Initializer. _owner_ is the owning mapping class (gets passed
|
277
615
|
# to the superclass initializer and therefore put into
|
@@ -282,10 +620,8 @@ module XML
|
|
282
620
|
# attribute of name attrname) is added to the mapping class
|
283
621
|
# (using attr_accessor).
|
284
622
|
#
|
285
|
-
#
|
286
|
-
#
|
287
|
-
# @options. Two entries -- :optional and :default_value -- in
|
288
|
-
# the options hash are already processed in SingleAttributeNode:
|
623
|
+
# In the initializer, two option arguments -- :optional and
|
624
|
+
# :default_value -- are processed in SingleAttributeNode:
|
289
625
|
#
|
290
626
|
# Supplying :default_value=>_obj_ makes _obj_ the _default
|
291
627
|
# value_ for this attribute. When unmarshalling (loading) an
|
@@ -296,43 +632,24 @@ module XML
|
|
296
632
|
#
|
297
633
|
# Providing just :optional=>true is equivalent to providing
|
298
634
|
# :default_value=>nil.
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
# initialize.
|
303
|
-
#
|
304
|
-
# For example (TextNode is a subclass of SingleAttributeNote):
|
305
|
-
#
|
306
|
-
# class Address
|
307
|
-
# include XML::Mapping
|
308
|
-
# text_node :city, "city", :optional=>true, :default_value=>"Berlin"
|
309
|
-
# end
|
310
|
-
#
|
311
|
-
# Here +Address+ is the _owner_, <tt>:city</tt> is the
|
312
|
-
# _attrname_,
|
313
|
-
# <tt>{:optional=>true,:default_value=>"Berlin"}</tt> is the
|
314
|
-
# @options, and ["city"] is the argument list that'll be passed
|
315
|
-
# to TextNode.initialize_impl. "city" is of course the XPath
|
316
|
-
# expression locating the XML sub-element this text node refers
|
317
|
-
# to; TextNode.initialize_impl stores it into @path.
|
318
|
-
def initialize(owner,attrname,*args)
|
319
|
-
super(owner)
|
320
|
-
@attrname = attrname
|
321
|
-
owner.add_accessor attrname
|
322
|
-
if Hash===args[-1]
|
323
|
-
@options = args[-1]
|
324
|
-
args = args[0..-2]
|
325
|
-
else
|
326
|
-
@options={}
|
327
|
-
end
|
635
|
+
def initialize(*args)
|
636
|
+
@attrname,*args = super(*args)
|
637
|
+
@owner.add_accessor @attrname
|
328
638
|
if @options[:optional] and not(@options.has_key?(:default_value))
|
329
639
|
@options[:default_value] = nil
|
330
640
|
end
|
331
641
|
initialize_impl(*args)
|
642
|
+
args
|
332
643
|
end
|
333
|
-
#
|
644
|
+
# this method was retained for compatibility with xml-mapping 0.8.
|
645
|
+
#
|
646
|
+
# It used to be the initializer to be implemented by subclasses. The
|
647
|
+
# arguments (args) are those still unprocessed by
|
648
|
+
# SingleAttributeNode's initializer.
|
649
|
+
#
|
650
|
+
# In xml-mapping 0.9 and up, you should just override initialize() and
|
651
|
+
# call super.initialize. The returned array is the same args array.
|
334
652
|
def initialize_impl(*args)
|
335
|
-
raise "abstract method called"
|
336
653
|
end
|
337
654
|
|
338
655
|
# Exception that may be used by implementations of
|
@@ -356,15 +673,14 @@ module XML
|
|
356
673
|
obj.send :"#{@attrname}=", @options[:default_value]
|
357
674
|
end
|
358
675
|
end
|
676
|
+
true
|
359
677
|
end
|
360
678
|
|
361
|
-
# (to be overridden by subclasses) Extract and return the
|
362
|
-
# attribute
|
363
|
-
# implementation
|
364
|
-
#
|
365
|
-
#
|
366
|
-
# _xml_, it should raise NoAttrValueSet in order to initiate
|
367
|
-
# proper handling of possibly supplied :optional and
|
679
|
+
# (to be overridden by subclasses) Extract and return the value
|
680
|
+
# of the attribute this node is responsible for (@attrname) from
|
681
|
+
# _xml_. If the implementation decides that the attribute value
|
682
|
+
# is "unset" in _xml_, it should raise NoAttrValueSet in order
|
683
|
+
# to initiate proper handling of possibly supplied :optional and
|
368
684
|
# :default_value options (you may use #default_when_xpath_err
|
369
685
|
# for this purpose).
|
370
686
|
def extract_attr_value(xml)
|
@@ -382,14 +698,17 @@ module XML
|
|
382
698
|
end
|
383
699
|
set_attr_value(xml, value)
|
384
700
|
end
|
701
|
+
true
|
385
702
|
end
|
386
|
-
# (to be overridden by subclasses) Write _value_
|
387
|
-
#
|
703
|
+
# (to be overridden by subclasses) Write _value_, which is the
|
704
|
+
# current value of the attribute this node is responsible for
|
705
|
+
# (@attrname), into (the correct sub-nodes, attributes,
|
706
|
+
# whatever) of _xml_.
|
388
707
|
def set_attr_value(xml, value)
|
389
708
|
raise "abstract method called"
|
390
709
|
end
|
391
|
-
def obj_initializing(obj) # :nodoc:
|
392
|
-
if @options.has_key?
|
710
|
+
def obj_initializing(obj,mapping) # :nodoc:
|
711
|
+
if @options.has_key?(:default_value) and (mapping==nil || mapping==@mapping)
|
393
712
|
begin
|
394
713
|
obj.send :"#{@attrname}=", @options[:default_value].clone
|
395
714
|
rescue
|
@@ -410,160 +729,11 @@ module XML
|
|
410
729
|
raise NoAttrValueSet, "Attribute #{@attrname} not set (XXPathError: #{err})"
|
411
730
|
end
|
412
731
|
end
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
# with the xml-mapping framework.
|
418
|
-
#
|
419
|
-
# A new "factory method" will automatically be added to
|
420
|
-
# ClassMethods (and therefore to all classes that include
|
421
|
-
# XML::Mapping from now on); so you can call it from the body of
|
422
|
-
# your mapping class definition in order to create nodes of type
|
423
|
-
# _c_. The name of the factory method is derived by "underscoring"
|
424
|
-
# the (unqualified) name of _c_;
|
425
|
-
# e.g. _c_==<tt>Foo::Bar::MyNiftyNode</tt> will result in the
|
426
|
-
# creation of a factory method named +my_nifty_node+. The
|
427
|
-
# generated factory method creates and returns a new instance of
|
428
|
-
# _c_. The list of argument to _c_.new consists of _self_
|
429
|
-
# (i.e. the mapping class the factory method was called from)
|
430
|
-
# followed by the arguments passed to the factory method. You
|
431
|
-
# should always use the factory methods to create instances of
|
432
|
-
# node classes; you should never need to call a node class's
|
433
|
-
# constructor directly.
|
434
|
-
#
|
435
|
-
# For a demonstration, see the calls to +text_node+, +array_node+
|
436
|
-
# etc. in the examples along with the corresponding node classes
|
437
|
-
# TextNode, ArrayNode etc. (these predefined node classes are in
|
438
|
-
# no way "special"; they're added using add_node_class in
|
439
|
-
# mapping.rb just like any custom node classes would be).
|
440
|
-
def self.add_node_class(c)
|
441
|
-
meth_name = c.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"_"+$2.downcase}
|
442
|
-
ClassMethods.module_eval <<-EOS
|
443
|
-
def #{meth_name}(*args)
|
444
|
-
#{c.name}.new(self,*args)
|
445
|
-
end
|
446
|
-
EOS
|
447
|
-
end
|
448
|
-
|
449
|
-
|
450
|
-
# The instance methods of this module are automatically added as
|
451
|
-
# class methods to a class that includes XML::Mapping.
|
452
|
-
module ClassMethods
|
453
|
-
#ClassMethods = Module.new do # this is the alterbative -- but see above for peculiarities
|
454
|
-
|
455
|
-
# Add getter and setter methods for a new attribute named _name_
|
456
|
-
# to this class. This is a convenience method intended to be
|
457
|
-
# called from Node class initializers.
|
458
|
-
def add_accessor(name)
|
459
|
-
name = name.id2name if name.kind_of? Symbol
|
460
|
-
unless self.instance_methods.include?(name)
|
461
|
-
self.module_eval <<-EOS
|
462
|
-
attr_reader :#{name}
|
463
|
-
EOS
|
464
|
-
end
|
465
|
-
unless self.instance_methods.include?("#{name}=")
|
466
|
-
self.module_eval <<-EOS
|
467
|
-
attr_writer :#{name}
|
468
|
-
EOS
|
469
|
-
end
|
470
|
-
end
|
471
|
-
|
472
|
-
# Create a new instance of this class from the XML contained in
|
473
|
-
# the file named _filename_. Calls load_from_xml internally.
|
474
|
-
def load_from_file(filename)
|
475
|
-
xml = REXML::Document.new(File.new(filename))
|
476
|
-
load_from_xml(xml.root)
|
477
|
-
end
|
478
|
-
|
479
|
-
# Create a new instance of this class from the XML contained in
|
480
|
-
# _xml_ (a REXML::Element).
|
481
|
-
#
|
482
|
-
# Allocates a new object, then calls fill_from_xml(_xml_) on
|
483
|
-
# it.
|
484
|
-
def load_from_xml(xml)
|
485
|
-
obj = self.allocate
|
486
|
-
obj.initialize_xml_mapping
|
487
|
-
obj.fill_from_xml(xml)
|
488
|
-
obj
|
489
|
-
end
|
490
|
-
|
491
|
-
|
492
|
-
# array of all nodes types defined in this class, in the order
|
493
|
-
# of their definition
|
494
|
-
def xml_mapping_nodes
|
495
|
-
@xml_mapping_nodes ||= []
|
496
|
-
end
|
497
|
-
|
498
|
-
|
499
|
-
# enumeration of all nodes types in effect when
|
500
|
-
# marshalling/unmarshalling this class, that is, node types
|
501
|
-
# defined for this class as well as for its superclasses. The
|
502
|
-
# node types are returned in the order of their definition,
|
503
|
-
# starting with the topmost superclass that has node types
|
504
|
-
# defined.
|
505
|
-
def all_xml_mapping_nodes
|
506
|
-
# TODO: we could return a dynamic Enumerable here, or cache
|
507
|
-
# the array...
|
508
|
-
result = []
|
509
|
-
if superclass and superclass.respond_to?(:all_xml_mapping_nodes)
|
510
|
-
result += superclass.all_xml_mapping_nodes
|
511
|
-
end
|
512
|
-
result += xml_mapping_nodes
|
513
|
-
end
|
514
|
-
|
515
|
-
|
516
|
-
# The "root element name" of this class (combined getter/setter
|
517
|
-
# method).
|
518
|
-
#
|
519
|
-
# The root element name is the name of the root element of the
|
520
|
-
# XML tree returned by <this class>.#save_to_xml (or, more
|
521
|
-
# specifically, <this class>.#pre_save). By default, this method
|
522
|
-
# returns the #default_root_element_name; you may call this
|
523
|
-
# method with an argument to set the root element name to
|
524
|
-
# something other than the default.
|
525
|
-
def root_element_name(name=nil)
|
526
|
-
if name
|
527
|
-
Classes_w_nondefault_rootelt_names.delete(root_element_name)
|
528
|
-
Classes_w_default_rootelt_names.delete(root_element_name)
|
529
|
-
Classes_w_default_rootelt_names.delete(name)
|
530
|
-
|
531
|
-
@root_element_name = name
|
532
|
-
|
533
|
-
Classes_w_nondefault_rootelt_names[name]=self
|
534
|
-
end
|
535
|
-
@root_element_name || default_root_element_name
|
536
|
-
end
|
537
|
-
|
538
|
-
|
539
|
-
# The default root element name for this class. Equals the class
|
540
|
-
# name, with all parent module names stripped, and with capital
|
541
|
-
# letters converted to lowercase and preceded by a dash;
|
542
|
-
# e.g. "Foo::Bar::MySampleClass" becomes "my-sample-class".
|
543
|
-
def default_root_element_name
|
544
|
-
self.name.split('::')[-1].gsub(/^(.)/){$1.downcase}.gsub(/(.)([A-Z])/){$1+"-"+$2.downcase}
|
732
|
+
# (overridden) returns true if and only if the value of this
|
733
|
+
# node's attribute in _obj_ is non-nil.
|
734
|
+
def is_present_in? obj
|
735
|
+
nil != obj.send(:"#{@attrname}")
|
545
736
|
end
|
546
|
-
|
547
|
-
end
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
# "polymorphic" load function. Turns the XML tree _xml_ into an
|
552
|
-
# object, which is returned. The class of the object is
|
553
|
-
# automatically determined from the root element name of _xml_
|
554
|
-
# using XML::Mapping::class_for_root_elt_name.
|
555
|
-
def self.load_object_from_xml(xml)
|
556
|
-
unless c = class_for_root_elt_name(xml.name)
|
557
|
-
raise MappingError, "no mapping class for root element name #{xml.name}"
|
558
|
-
end
|
559
|
-
c.load_from_xml(xml)
|
560
|
-
end
|
561
|
-
|
562
|
-
# Like load_object_from_xml, but loads from the XML file named by
|
563
|
-
# _filename_.
|
564
|
-
def self.load_object_from_file(filename)
|
565
|
-
xml = REXML::Document.new(File.new(filename))
|
566
|
-
load_object_from_xml(xml.root)
|
567
737
|
end
|
568
738
|
|
569
739
|
end
|