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.
Files changed (71) hide show
  1. data/ChangeLog +64 -3
  2. data/README +871 -173
  3. data/README_XPATH +40 -13
  4. data/Rakefile +37 -26
  5. data/TODO.txt +39 -8
  6. data/examples/README +5 -0
  7. data/examples/company_usage.intout +34 -22
  8. data/examples/documents_folders.rb +31 -0
  9. data/examples/documents_folders.xml +16 -0
  10. data/examples/documents_folders_usage.intin.rb +18 -0
  11. data/examples/documents_folders_usage.intout +46 -0
  12. data/examples/order_signature_enhanced_usage.intout +21 -11
  13. data/examples/order_usage.intin.rb +52 -5
  14. data/examples/order_usage.intout +154 -80
  15. data/examples/person.intin.rb +44 -0
  16. data/examples/person.intout +27 -0
  17. data/examples/person_mm.intin.rb +119 -0
  18. data/examples/person_mm.intout +114 -0
  19. data/examples/publication.intin.rb +44 -0
  20. data/examples/publication.intout +20 -0
  21. data/examples/reader.intin.rb +33 -0
  22. data/examples/reader.intout +19 -0
  23. data/examples/stringarray.rb +5 -0
  24. data/examples/stringarray.xml +10 -0
  25. data/examples/stringarray_usage.intin.rb +11 -0
  26. data/examples/stringarray_usage.intout +31 -0
  27. data/examples/time_augm.intout +19 -7
  28. data/examples/time_augm_loading.intin.rb +44 -0
  29. data/examples/time_augm_loading.intout +12 -0
  30. data/examples/time_node.intin.rb +79 -0
  31. data/examples/time_node.rb +3 -2
  32. data/examples/time_node_w_marshallers.intin.rb +48 -0
  33. data/examples/time_node_w_marshallers.intout +25 -0
  34. data/examples/time_node_w_marshallers.xml +9 -0
  35. data/examples/xpath_create_new.intout +132 -114
  36. data/examples/xpath_ensure_created.intout +86 -65
  37. data/examples/xpath_pathological.intout +16 -16
  38. data/examples/xpath_usage.intout +1 -1
  39. data/install.rb +1 -0
  40. data/lib/xml/mapping.rb +3 -1
  41. data/lib/xml/mapping/base.rb +442 -272
  42. data/lib/xml/mapping/core_classes_mapping.rb +32 -0
  43. data/lib/xml/mapping/standard_nodes.rb +176 -86
  44. data/lib/xml/mapping/version.rb +2 -2
  45. data/lib/xml/rexml_ext.rb +186 -0
  46. data/lib/xml/xxpath.rb +28 -265
  47. data/lib/xml/xxpath/steps.rb +345 -0
  48. data/lib/xml/xxpath_methods.rb +96 -0
  49. data/test/all_tests.rb +4 -1
  50. data/test/benchmark_fixtures.rb +14 -0
  51. data/test/{multiple_mappings.rb → bookmarks.rb} +0 -0
  52. data/test/company.rb +47 -0
  53. data/test/documents_folders.rb +11 -1
  54. data/test/examples_test.rb +29 -0
  55. data/test/fixtures/benchmark.xml +77 -0
  56. data/test/fixtures/company1.xml +9 -0
  57. data/test/fixtures/documents_folders.xml +0 -8
  58. data/test/fixtures/documents_folders2.xml +13 -19
  59. data/test/fixtures/triangle_m1.xml +17 -0
  60. data/test/fixtures/triangle_m2.xml +19 -0
  61. data/test/inheritance_test.rb +50 -0
  62. data/test/multiple_mappings_test.rb +155 -0
  63. data/test/rexml_xpath_benchmark.rb +29 -0
  64. data/test/triangle_mm.rb +57 -0
  65. data/test/xml_mapping_adv_test.rb +36 -1
  66. data/test/xml_mapping_test.rb +136 -7
  67. data/test/xpath_test.rb +154 -0
  68. data/test/xxpath_benchmark.rb +36 -0
  69. data/test/xxpath_benchmark.result1.txt +17 -0
  70. data/test/xxpath_methods_test.rb +61 -0
  71. 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
- <foo>
21
- <bar>
22
- <baz key='work'>Java</baz>
23
- <baz key='play'>Ruby</baz>
24
- </bar>
25
- </foo>
26
-
27
- ### no change (path existed before)
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
- <foo>
34
- <bar>
35
- <baz key='work'>Java</baz>
36
- <baz key='play'>Ruby</baz>
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
- </bar>
39
- </foo>
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
- <foo>
47
- <bar>
48
- <baz key='work'>Java</baz>
49
- <baz key='play'>Ruby</baz>
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
- </bar>
52
- </foo>
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 key2='hello' key='work'> ... </>
66
+ => <baz key='work' key2='hello'> ... </>
58
67
  d.write($stdout,2)
59
- <foo>
60
- <bar>
61
- <baz key2='hello' key='work'>Java</baz>
62
- <baz key='play'>Ruby</baz>
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
- </bar>
65
- </foo>
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
- <foo>
74
- <bar>
75
- <baz key2='hello' key='work'>Java</baz>
76
- <baz key='play'>Ruby</baz>
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
- </bar>
79
- </foo>
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:0x3223b0 @parent=<baz haha='[unset]'/>, @name="haha">
99
+ => #<XML::XXPath::Accessors::Attribute:0x7f4876522658 @name="haha", @parent=<baz haha='[unset]'/>>
85
100
  d.write($stdout,2)
86
- <foo>
87
- <bar>
88
- <baz key2='hello' key='work'>Java</baz>
89
- <baz key='play'>Ruby</baz>
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
- <baz/>
92
- <baz/>
93
- <baz haha='[unset]'/>
94
- </bar>
95
- </foo>
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:0x31f830 @parent=<baz haha='[unset]'/>, @name="haha">
118
+ => #<XML::XXPath::Accessors::Attribute:0x7f4876516bc8 @name="haha", @parent=<baz haha='[unset]'/>>
101
119
  d.write($stdout,2)
102
- <foo>
103
- <bar>
104
- <baz key2='hello' key='work'>Java</baz>
105
- <baz key='play'>Ruby</baz>
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
- <baz/>
108
- <baz/>
109
- <baz haha='[unset]'/>
110
- </bar>
111
- </foo>
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
- <foo>
28
- <bar>
29
- </>
30
- </bar>
31
- <bar/>
32
- </foo>
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
- <foo>
47
- <bar>
48
- <new-one>hello!</new-one>
49
- </bar>
50
- <bar/>
51
- </foo>
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
 
@@ -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/../xml/xxpath.rb:130:in `first'
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
@@ -26,6 +26,7 @@ end
26
26
  files = %w-
27
27
  xml/mapping.rb
28
28
  xml/xxpath.rb
29
+ xml/xxpath_methods.rb
29
30
  xml/mapping/base.rb
30
31
  xml/mapping/standard_nodes.rb
31
32
  xml/mapping/version.rb
@@ -1,14 +1,16 @@
1
1
  # xml-mapping -- bidirectional Ruby-XML mapper
2
- # Copyright (C) 2004,2005 Olaf Klischat
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
@@ -1,5 +1,5 @@
1
1
  # xml-mapping -- bidirectional Ruby-XML mapper
2
- # Copyright (C) 2004,2005 Olaf Klischat
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
- # can't really use class variables for these because they must be
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
- Classes_w_default_rootelt_names = {} #:nodoc:
79
- Classes_w_nondefault_rootelt_names = {} #:nodoc:
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
- Classes_w_default_rootelt_names[base.default_root_element_name] = base
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
- # Finds the mapping class corresponding to the given XML root
89
- # element name. This is the inverse operation to
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
- Classes_w_nondefault_rootelt_names[name] ||
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
- def initialize_xml_mapping #:nodoc:
101
- self.class.all_xml_mapping_nodes.each do |node|
102
- node.obj_initializing(self)
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
- # private :initialize_xml_mapping
107
-
108
- # Initializer. Calls obj_initializing(self) on all nodes. You
109
- # should call this using +super+ in your mapping classes to
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
- pre_load(xml)
124
- self.class.all_xml_mapping_nodes.each do |node|
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(xml)
169
- post_save(xml)
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
- xml.write(f,2)
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 factory method"
209
- # (e.g. "text_node") will then be added as a class method to your
210
- # mapping classes. The node factory method is called from the body
211
- # of the mapping classes as demonstrated in the examples. It
212
- # creates an instance of its corresponding node type (the list of
213
- # parameters to the node factory method, preceded by the owning
214
- # mapping class, will be passed to the constructor of the node
215
- # type) and adds it to its owning mapping class, so there is one
216
- # node object per node definition per mapping class. That node
217
- # object will handle all XML marshalling/unmarshalling for this
218
- # node, for all instances of the mapping class. For this purpose,
219
- # the marshalling and unmarshalling methods of a mapping class
220
- # instance (fill_into_xml and fill_from_xml, respectively)
221
- # will call obj_to_xml resp. xml_to_obj on all nodes of the
222
- # mapping class, in the order of their definition, passing the
223
- # REXML element the data is to be marshalled to/unmarshalled from
224
- # as well as the object the data is to be read from/filled into.
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
- def initialize(owner)
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
- owner.xml_mapping_nodes << self
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 node. _obj_ is the instance, _xml_ is the element (a
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 node. _obj_ is the instance, _xml_ is the element
250
- # (a REXML::Element). The node must extract "its" data from
251
- # _obj_ and store it to the corresponding parts (sub-elements,
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 is being initialized. _obj_ is the
258
- # instance. You may set up initial values for the attributes
259
- # this node is responsible for here. Default implementation is
260
- # empty.
261
- def obj_initializing(obj)
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. This class also introduces a
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 inherit from
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
- # If the last argument is a hash, it is assumed to be the
286
- # abovementioned "options hash", and is stored into
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
- # The remaining arguments are passed to initialize_impl, which
301
- # is the initializer subclasses should overwrite instead of
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
- # Initializer to be implemented by subclasses.
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's value from _xml_. In the example above, TextNode's
363
- # implementation would return the current value of the
364
- # sub-element named by @path (i.e., "city"). If the
365
- # implementation decides that the attribute value is "unset" in
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_ into the
387
- # correct sub-nodes of _xml_.
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? :default_value
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
- end
414
-
415
-
416
- # Registers the new node class _c_ (must be a descendant of Node)
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