nokogiri-happymapper 0.3.6 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +521 -0
- data/lib/happymapper.rb +493 -43
- data/lib/happymapper/item.rb +61 -8
- data/spec/fixtures/ambigous_items.xml +1 -1
- data/spec/fixtures/atom.xml +19 -0
- data/spec/fixtures/inagy.xml +85 -0
- data/spec/fixtures/optional_attributes.xml +6 -0
- data/spec/fixtures/subclass_namespace.xml +50 -0
- data/spec/happymapper_spec.rb +220 -6
- data/spec/happymapper_to_xml_namespaces_spec.rb +196 -0
- data/spec/happymapper_to_xml_spec.rb +196 -0
- data/spec/ignay_spec.rb +95 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/xpath_spec.rb +88 -0
- metadata +70 -68
- data/README +0 -62
- data/spec/spec.opts +0 -1
data/lib/happymapper.rb
CHANGED
@@ -18,21 +18,78 @@ module HappyMapper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module ClassMethods
|
21
|
+
|
22
|
+
#
|
23
|
+
# The xml has the following attributes defined.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
#
|
27
|
+
# "<country code='de'>Germany</country>"
|
28
|
+
#
|
29
|
+
# # definition of the 'code' attribute within the class
|
30
|
+
# attribute :code, String
|
31
|
+
#
|
32
|
+
# @param [Symbol] name the name of the accessor that is created
|
33
|
+
# @param [String,Class] type the class name of the name of the class whcih
|
34
|
+
# the object will be converted upon parsing
|
35
|
+
# @param [Hash] options additional parameters to send to the relationship
|
36
|
+
#
|
21
37
|
def attribute(name, type, options={})
|
22
38
|
attribute = Attribute.new(name, type, options)
|
23
39
|
@attributes[to_s] ||= []
|
24
40
|
@attributes[to_s] << attribute
|
25
41
|
attr_accessor attribute.method_name.intern
|
26
42
|
end
|
27
|
-
|
43
|
+
|
44
|
+
#
|
45
|
+
# The elements defined through {#attribute}.
|
46
|
+
#
|
47
|
+
# @return [Array<Attribute>] a list of the attributes defined for this class;
|
48
|
+
# an empty array is returned when there have been no attributes defined.
|
49
|
+
#
|
28
50
|
def attributes
|
29
51
|
@attributes[to_s] || []
|
30
52
|
end
|
31
53
|
|
54
|
+
#
|
55
|
+
# Register a namespace that is used to persist the object namespace back to
|
56
|
+
# XML.
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
#
|
60
|
+
# register_namespace 'prefix', 'http://www.unicornland.com/prefix'
|
61
|
+
#
|
62
|
+
# # the output will contain the namespace defined
|
63
|
+
#
|
64
|
+
# "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
|
65
|
+
# ...
|
66
|
+
# </outputXML>"
|
67
|
+
#
|
68
|
+
# @param [String] namespace the xml prefix
|
69
|
+
# @param [String] ns url for the xml namespace
|
70
|
+
#
|
32
71
|
def register_namespace(namespace, ns)
|
33
72
|
@registered_namespaces.merge!({namespace => ns})
|
34
73
|
end
|
35
|
-
|
74
|
+
|
75
|
+
#
|
76
|
+
# An element defined in the XML that is parsed.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
#
|
80
|
+
# "<address location='home'>
|
81
|
+
# <city>Oldenburg</city>
|
82
|
+
# </address>"
|
83
|
+
#
|
84
|
+
# # definition of the 'city' element within the class
|
85
|
+
#
|
86
|
+
# element :city, String
|
87
|
+
#
|
88
|
+
# @param [Symbol] name the name of the accessor that is created
|
89
|
+
# @param [String,Class] type the class name of the name of the class whcih
|
90
|
+
# the object will be converted upon parsing
|
91
|
+
# @param [Hash] options additional parameters to send to the relationship
|
92
|
+
#
|
36
93
|
def element(name, type, options={})
|
37
94
|
element = Element.new(name, type, options)
|
38
95
|
@elements[to_s] ||= []
|
@@ -40,79 +97,177 @@ module HappyMapper
|
|
40
97
|
attr_accessor element.method_name.intern
|
41
98
|
end
|
42
99
|
|
100
|
+
#
|
101
|
+
# The elements defined through {#element}, {#has_one}, and {#has_many}.
|
102
|
+
#
|
103
|
+
# @return [Array<Element>] a list of the elements contained defined for this
|
104
|
+
# class; an empty array is returned when there have been no elements
|
105
|
+
# defined.
|
106
|
+
#
|
43
107
|
def elements
|
44
108
|
@elements[to_s] || []
|
45
109
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
110
|
+
|
111
|
+
#
|
112
|
+
# The value stored in the text node of the current element.
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
#
|
116
|
+
# "<firstName>Michael Jackson</firstName>"
|
117
|
+
#
|
118
|
+
# # definition of the 'firstName' text node within the class
|
119
|
+
#
|
120
|
+
# content :first_name, String
|
121
|
+
#
|
122
|
+
# @param [Symbol] name the name of the accessor that is created
|
123
|
+
# @param [String,Class] type the class name of the name of the class whcih
|
124
|
+
# the object will be converted upon parsing
|
125
|
+
# @param [Hash] options additional parameters to send to the relationship
|
126
|
+
#
|
127
|
+
def content(name, type, options={})
|
128
|
+
@content = TextNode.new(name, type, options)
|
129
|
+
attr_accessor @content.method_name.intern
|
50
130
|
end
|
51
131
|
|
132
|
+
#
|
133
|
+
# Sets the object to have xml content, this will assign the XML contents
|
134
|
+
# that are parsed to the attribute accessor xml_content. The object will
|
135
|
+
# respond to the method #xml_content and will return the XML data that
|
136
|
+
# it has parsed.
|
137
|
+
#
|
52
138
|
def has_xml_content
|
53
139
|
attr_accessor :xml_content
|
54
140
|
end
|
55
141
|
|
142
|
+
#
|
143
|
+
# The object has one of these elements in the XML. If there are multiple,
|
144
|
+
# the last one will be set to this value.
|
145
|
+
#
|
146
|
+
# @param [Symbol] name the name of the accessor that is created
|
147
|
+
# @param [String,Class] type the class name of the name of the class whcih
|
148
|
+
# the object will be converted upon parsing
|
149
|
+
# @param [Hash] options additional parameters to send to the relationship
|
150
|
+
#
|
151
|
+
# @see #element
|
152
|
+
#
|
56
153
|
def has_one(name, type, options={})
|
57
154
|
element name, type, {:single => true}.merge(options)
|
58
155
|
end
|
59
|
-
|
156
|
+
|
157
|
+
#
|
158
|
+
# The object has many of these elements in the XML.
|
159
|
+
#
|
160
|
+
# @param [Symbol] name the name of accessor that is created
|
161
|
+
# @param [String,Class] type the class name or the name of the class which
|
162
|
+
# the object will be converted upon parsing.
|
163
|
+
# @param [Hash] options additional parameters to send to the relationship
|
164
|
+
#
|
165
|
+
# @see #element
|
166
|
+
#
|
60
167
|
def has_many(name, type, options={})
|
61
168
|
element name, type, {:single => false}.merge(options)
|
62
169
|
end
|
63
|
-
|
170
|
+
|
171
|
+
#
|
64
172
|
# Specify a namespace if a node and all its children are all namespaced
|
65
173
|
# elements. This is simpler than passing the :namespace option to each
|
66
174
|
# defined element.
|
175
|
+
#
|
176
|
+
# @param [String] namespace the namespace to set as default for the class
|
177
|
+
# element.
|
178
|
+
#
|
67
179
|
def namespace(namespace = nil)
|
68
180
|
@namespace = namespace if namespace
|
69
181
|
@namespace
|
70
182
|
end
|
71
183
|
|
184
|
+
#
|
185
|
+
# @param [String] new_tag_name the name for the tag
|
186
|
+
#
|
72
187
|
def tag(new_tag_name)
|
73
188
|
@tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
|
74
189
|
end
|
75
190
|
|
191
|
+
#
|
192
|
+
# The name of the tag
|
193
|
+
#
|
194
|
+
# @return [String] the name of the tag as a string, downcased
|
195
|
+
#
|
76
196
|
def tag_name
|
77
197
|
@tag_name ||= to_s.split('::')[-1].downcase
|
78
198
|
end
|
79
199
|
|
200
|
+
#
|
201
|
+
# @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
|
202
|
+
# contents to convert into Object.
|
203
|
+
# @param [Hash] options additional information for parsing. :single => true
|
204
|
+
# if requesting a single object, otherwise it defaults to retuning an
|
205
|
+
# array of multiple items. :xpath information where to start the parsing
|
206
|
+
# :namespace is the namespace to use for additional information.
|
207
|
+
#
|
80
208
|
def parse(xml, options = {})
|
81
|
-
|
209
|
+
|
210
|
+
# create a local copy of the objects namespace value for this parse execution
|
82
211
|
namespace = @namespace
|
83
|
-
|
84
|
-
|
212
|
+
|
213
|
+
# If the XML specified is an Node then we have what we need.
|
214
|
+
if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
|
85
215
|
node = xml
|
86
216
|
else
|
217
|
+
|
218
|
+
# If xml is an XML document select the root node of the document
|
87
219
|
if xml.is_a?(Nokogiri::XML::Document)
|
88
220
|
node = xml.root
|
89
221
|
else
|
90
|
-
|
222
|
+
|
223
|
+
# Attempt to parse the xml value with Nokogiri XML as a document
|
224
|
+
# and select the root element
|
225
|
+
|
226
|
+
xml = Nokogiri::XML(xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
|
91
227
|
node = xml.root
|
92
228
|
end
|
93
229
|
|
230
|
+
# if the node name is equal to the tag name then the we are parsing the
|
231
|
+
# root element and that is important to record so that we can apply
|
232
|
+
# the correct xpath on the elements of this document.
|
233
|
+
|
94
234
|
root = node.name == tag_name
|
95
235
|
end
|
96
236
|
|
97
|
-
#
|
98
|
-
#
|
99
|
-
namespaces
|
100
|
-
|
101
|
-
namespaces
|
102
|
-
namespaces
|
103
|
-
|
104
|
-
|
237
|
+
# if any namespaces have been provied then we should capture those and then
|
238
|
+
# merge them with any namespaces found on the xml node and merge all that
|
239
|
+
# with any namespaces that have been registered on the object
|
240
|
+
|
241
|
+
namespaces = options[:namespaces] || {}
|
242
|
+
namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
|
243
|
+
namespaces = namespaces.merge(@registered_namespaces)
|
244
|
+
|
245
|
+
# if a namespace has been provided then set the current namespace to it
|
246
|
+
# or set the default namespace to the one defined under 'xmlns'
|
247
|
+
# or set the default namespace to the namespace that matches 'happymapper's
|
248
|
+
|
249
|
+
if options[:namespace]
|
250
|
+
namespace = options[:namespace]
|
251
|
+
elsif namespaces.has_key?("xmlns")
|
105
252
|
namespace ||= DEFAULT_NS
|
106
253
|
namespaces[namespace] = namespaces.delete("xmlns")
|
107
254
|
elsif namespaces.has_key?(DEFAULT_NS)
|
108
255
|
namespace ||= DEFAULT_NS
|
109
256
|
end
|
110
|
-
|
257
|
+
|
258
|
+
# from the options grab any nodes present and if none are present then
|
259
|
+
# perform the following to find the nodes for the given class
|
260
|
+
|
111
261
|
nodes = options.fetch(:nodes) do
|
262
|
+
|
263
|
+
# when at the root use the xpath '/' otherwise use a more gready './/'
|
264
|
+
# unless an xpath has been specified, which should overwrite default
|
265
|
+
# and finally attach the current namespace if one has been defined
|
266
|
+
#
|
267
|
+
|
112
268
|
xpath = (root ? '/' : './/')
|
113
269
|
xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
|
114
270
|
xpath += "#{namespace}:" if namespace
|
115
|
-
#puts "parse: #{xpath}"
|
116
271
|
|
117
272
|
nodes = []
|
118
273
|
|
@@ -120,48 +275,343 @@ module HappyMapper
|
|
120
275
|
# 1. specified tag
|
121
276
|
# 2. name of element
|
122
277
|
# 3. tag_name (derived from class name by default)
|
278
|
+
|
279
|
+
|
123
280
|
[options[:tag], options[:name], tag_name].compact.each do |xpath_ext|
|
124
|
-
|
281
|
+
begin
|
282
|
+
nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
|
283
|
+
rescue
|
284
|
+
break
|
285
|
+
end
|
125
286
|
break if nodes && !nodes.empty?
|
126
287
|
end
|
127
288
|
|
128
289
|
nodes
|
129
290
|
end
|
130
291
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
292
|
+
# Nothing matching found, we can go ahead and return
|
293
|
+
return ( ( options[:single] || root ) ? nil : [] ) if nodes.size == 0
|
294
|
+
|
295
|
+
# If the :limit option has been specified then we are going to slice
|
296
|
+
# our node results by that amount to allow us the ability to deal with
|
297
|
+
# a large result set of data.
|
298
|
+
|
299
|
+
limit = options[:in_groups_of] || nodes.size
|
300
|
+
|
301
|
+
# If the limit of 0 has been specified then the user obviously wants
|
302
|
+
# none of the nodes that we are serving within this batch of nodes.
|
303
|
+
|
304
|
+
return [] if limit == 0
|
305
|
+
|
306
|
+
collection = []
|
307
|
+
|
308
|
+
nodes.each_slice(limit) do |slice|
|
309
|
+
|
310
|
+
part = slice.map do |n|
|
311
|
+
obj = new
|
312
|
+
|
313
|
+
attributes.each do |attr|
|
314
|
+
obj.send("#{attr.method_name}=",attr.from_xml_node(n, namespace, namespaces))
|
315
|
+
end
|
316
|
+
|
317
|
+
elements.each do |elem|
|
318
|
+
obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
|
319
|
+
end
|
320
|
+
|
321
|
+
if @content
|
322
|
+
obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces))
|
323
|
+
end
|
324
|
+
|
325
|
+
# If the HappyMapper class has the method #xml_value=,
|
326
|
+
# attr_writer :xml_value, or attr_accessor :xml_value then we want to
|
327
|
+
# assign the current xml that we just parsed to the xml_value
|
328
|
+
|
329
|
+
if obj.respond_to?('xml_value=')
|
330
|
+
n.namespaces.each {|name,path| n[name] = path }
|
331
|
+
obj.xml_value = n.to_xml
|
332
|
+
end
|
333
|
+
|
334
|
+
# If the HappyMapper class has the method #xml_content=,
|
335
|
+
# attr_write :xml_content, or attr_accessor :xml_content then we want to
|
336
|
+
# assign the child xml that we just parsed to the xml_content
|
337
|
+
|
338
|
+
if obj.respond_to?('xml_content=')
|
339
|
+
n = n.children if n.respond_to?(:children)
|
340
|
+
obj.xml_content = n.to_xml
|
341
|
+
end
|
342
|
+
|
343
|
+
# collect the object that we have created
|
344
|
+
|
345
|
+
obj
|
137
346
|
end
|
347
|
+
|
348
|
+
# If a block has been provided and the user has requested that the objects
|
349
|
+
# be handled in groups then we should yield the slice of the objects to them
|
350
|
+
# otherwise continue to lump them together
|
138
351
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
obj.send("#{@text_node.method_name}=",
|
145
|
-
@text_node.from_xml_node(n, namespace, namespaces)) if @text_node
|
146
|
-
|
147
|
-
if obj.respond_to?('xml_content=')
|
148
|
-
n = n.children if n.respond_to?(:children)
|
149
|
-
obj.xml_content = n.to_xml
|
352
|
+
if block_given? and options[:in_groups_of]
|
353
|
+
yield part
|
354
|
+
else
|
355
|
+
collection += part
|
150
356
|
end
|
151
|
-
|
152
|
-
obj
|
357
|
+
|
153
358
|
end
|
154
359
|
|
155
360
|
# per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
|
156
361
|
nodes = nil
|
157
362
|
|
158
|
-
|
363
|
+
# If the :single option has been specified or we are at the root element
|
364
|
+
# then we are going to return the first item in the collection. Otherwise
|
365
|
+
# the return response is going to be an entire array of items.
|
366
|
+
|
367
|
+
if options[:single] or root
|
159
368
|
collection.first
|
160
369
|
else
|
161
370
|
collection
|
162
371
|
end
|
163
372
|
end
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
#
|
377
|
+
# Create an xml representation of the specified class based on defined
|
378
|
+
# HappyMapper elements and attributes. The method is defined in a way
|
379
|
+
# that it can be called recursively by classes that are also HappyMapper
|
380
|
+
# classes, allowg for the composition of classes.
|
381
|
+
#
|
382
|
+
# @param [Nokogiri::XML::Builder] builder an instance of the XML builder which
|
383
|
+
# is being used when called recursively.
|
384
|
+
# @param [String] default_namespace the name of the namespace which is the
|
385
|
+
# default for the xml being produced; this is specified by the element
|
386
|
+
# declaration when calling #to_xml recursively.
|
387
|
+
#
|
388
|
+
# @return [String,Nokogiri::XML::Builder] return XML representation of the
|
389
|
+
# HappyMapper object; when called recursively this is going to return
|
390
|
+
# and Nokogiri::XML::Builder object.
|
391
|
+
#
|
392
|
+
def to_xml(builder = nil,default_namespace = nil)
|
393
|
+
|
394
|
+
#
|
395
|
+
# If to_xml has been called without a passed in builder instance that
|
396
|
+
# means we are going to return xml output. When it has been called with
|
397
|
+
# a builder instance that means we most likely being called recursively
|
398
|
+
# and will return the end product as a builder instance.
|
399
|
+
#
|
400
|
+
unless builder
|
401
|
+
write_out_to_xml = true
|
402
|
+
builder = Nokogiri::XML::Builder.new
|
403
|
+
end
|
404
|
+
|
405
|
+
#
|
406
|
+
# Find the attributes for the class and collect them into an array
|
407
|
+
# that will be placed into a Hash structure
|
408
|
+
#
|
409
|
+
attributes = self.class.attributes.collect do |attribute|
|
410
|
+
|
411
|
+
#
|
412
|
+
# If an attribute is marked as read_only then we want to ignore the attribute
|
413
|
+
# when it comes to saving the xml document; so we wiill not go into any of
|
414
|
+
# the below process
|
415
|
+
#
|
416
|
+
unless attribute.options[:read_only]
|
417
|
+
|
418
|
+
value = send(attribute.method_name)
|
419
|
+
|
420
|
+
#
|
421
|
+
# If the attribute defines an on_save lambda/proc or value that maps to
|
422
|
+
# a method that the class has defined, then call it with the value as a
|
423
|
+
# parameter.
|
424
|
+
#
|
425
|
+
if on_save_action = attribute.options[:on_save]
|
426
|
+
if on_save_action.is_a?(Proc)
|
427
|
+
value = on_save_action.call(value)
|
428
|
+
elsif respond_to?(on_save_action)
|
429
|
+
value = send(on_save_action,value)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
#
|
434
|
+
# Attributes that have a nil value should be ignored unless they explicitly
|
435
|
+
# state that they should be expressed in the output.
|
436
|
+
#
|
437
|
+
if value || attribute.options[:state_when_nil]
|
438
|
+
attribute_namespace = attribute.options[:namespace] || default_namespace
|
439
|
+
[ "#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value ]
|
440
|
+
else
|
441
|
+
[]
|
442
|
+
end
|
443
|
+
|
444
|
+
else
|
445
|
+
[]
|
446
|
+
end
|
447
|
+
|
448
|
+
end.flatten
|
449
|
+
|
450
|
+
attributes = Hash[ *attributes ]
|
451
|
+
|
452
|
+
#
|
453
|
+
# Create a tag in the builder that matches the class's tag name and append
|
454
|
+
# any attributes to the element that were defined above.
|
455
|
+
#
|
456
|
+
builder.send("#{self.class.tag_name}_",attributes) do |xml|
|
457
|
+
|
458
|
+
#
|
459
|
+
# Add all the registered namespaces to the root element.
|
460
|
+
# When this is called recurisvely by composed classes the namespaces
|
461
|
+
# are still added to the root element
|
462
|
+
#
|
463
|
+
# However, we do not want to add the namespace if the namespace is 'xmlns'
|
464
|
+
# which means that it is the default namesapce of the code.
|
465
|
+
#
|
466
|
+
if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
|
467
|
+
self.class.instance_variable_get('@registered_namespaces').each_pair do |name,href|
|
468
|
+
name = nil if name == "xmlns"
|
469
|
+
builder.doc.root.add_namespace(name,href)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
#
|
474
|
+
# If the object we are persisting has a namespace declaration we will want
|
475
|
+
# to use that namespace or we will use the default namespace.
|
476
|
+
# When neither are specifed we are simply using whatever is default to the
|
477
|
+
# builder
|
478
|
+
#
|
479
|
+
if self.class.respond_to?(:namespace) && self.class.namespace
|
480
|
+
xml.parent.namespace = builder.doc.root.namespace_definitions.find { |x| x.prefix == self.class.namespace }
|
481
|
+
elsif default_namespace
|
482
|
+
xml.parent.namespace = builder.doc.root.namespace_definitions.find { |x| x.prefix == default_namespace }
|
483
|
+
end
|
484
|
+
|
485
|
+
|
486
|
+
#
|
487
|
+
# When a content has been defined we add the resulting value
|
488
|
+
# the output xml
|
489
|
+
#
|
490
|
+
if content = self.class.instance_variable_get('@content')
|
491
|
+
|
492
|
+
unless content.options[:read_only]
|
493
|
+
text_accessor = content.tag || content.name
|
494
|
+
value = send(text_accessor)
|
495
|
+
|
496
|
+
if on_save_action = content.options[:on_save]
|
497
|
+
if on_save_action.is_a?(Proc)
|
498
|
+
value = on_save_action.call(value)
|
499
|
+
elsif respond_to?(on_save_action)
|
500
|
+
value = send(on_save_action,value)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
builder.text(value)
|
505
|
+
end
|
506
|
+
|
507
|
+
end
|
508
|
+
|
509
|
+
#
|
510
|
+
# for every define element (i.e. has_one, has_many, element) we are
|
511
|
+
# going to persist each one
|
512
|
+
#
|
513
|
+
self.class.elements.each do |element|
|
514
|
+
|
515
|
+
#
|
516
|
+
# If an element is marked as read only do not consider at all when
|
517
|
+
# saving to XML.
|
518
|
+
#
|
519
|
+
unless element.options[:read_only]
|
520
|
+
|
521
|
+
tag = element.tag || element.name
|
522
|
+
|
523
|
+
#
|
524
|
+
# The value to store is the result of the method call to the element,
|
525
|
+
# by default this is simply utilizing the attr_accessor defined. However,
|
526
|
+
# this allows for this method to be overridden
|
527
|
+
#
|
528
|
+
value = send(element.name)
|
529
|
+
|
530
|
+
#
|
531
|
+
# If the element defines an on_save lambda/proc then we will call that
|
532
|
+
# operation on the specified value. This allows for operations to be
|
533
|
+
# performed to convert the value to a specific value to be saved to the xml.
|
534
|
+
#
|
535
|
+
if on_save_action = element.options[:on_save]
|
536
|
+
if on_save_action.is_a?(Proc)
|
537
|
+
value = on_save_action.call(value)
|
538
|
+
elsif respond_to?(on_save_action)
|
539
|
+
value = send(on_save_action,value)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
#
|
544
|
+
# Normally a nil value would be ignored, however if specified then
|
545
|
+
# an empty element will be written to the xml
|
546
|
+
#
|
547
|
+
if value.nil? && element.options[:single] && element.options[:state_when_nil]
|
548
|
+
xml.send("#{tag}_","")
|
549
|
+
end
|
550
|
+
|
551
|
+
#
|
552
|
+
# To allow for us to treat both groups of items and singular items
|
553
|
+
# equally we wrap the value and treat it as an array.
|
554
|
+
#
|
555
|
+
if value.nil?
|
556
|
+
values = []
|
557
|
+
elsif value.respond_to?(:to_ary) && !element.options[:single]
|
558
|
+
values = value.to_ary
|
559
|
+
else
|
560
|
+
values = [value]
|
561
|
+
end
|
562
|
+
|
563
|
+
values.each do |item|
|
564
|
+
|
565
|
+
if item.is_a?(HappyMapper)
|
566
|
+
|
567
|
+
#
|
568
|
+
# Other items are convertable to xml through the xml builder
|
569
|
+
# process should have their contents retrieved and attached
|
570
|
+
# to the builder structure
|
571
|
+
#
|
572
|
+
item.to_xml(xml,element.options[:namespace])
|
573
|
+
|
574
|
+
elsif item
|
575
|
+
|
576
|
+
item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
|
577
|
+
|
578
|
+
#
|
579
|
+
# When a value exists we should append the value for the tag
|
580
|
+
#
|
581
|
+
if item_namespace
|
582
|
+
xml[item_namespace].send("#{tag}_",item.to_s)
|
583
|
+
else
|
584
|
+
xml.send("#{tag}_",item.to_s)
|
585
|
+
end
|
586
|
+
|
587
|
+
else
|
588
|
+
|
589
|
+
#
|
590
|
+
# Normally a nil value would be ignored, however if specified then
|
591
|
+
# an empty element will be written to the xml
|
592
|
+
#
|
593
|
+
xml.send("#{tag}_","") if element.options[:state_when_nil]
|
594
|
+
|
595
|
+
end
|
596
|
+
|
597
|
+
end
|
598
|
+
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
end
|
603
|
+
|
604
|
+
# Write out to XML, this value was set above, based on whether or not an XML
|
605
|
+
# builder object was passed to it as a parameter. When there was no parameter
|
606
|
+
# we assume we are at the root level of the #to_xml call and want the actual
|
607
|
+
# xml generated from the object. If an XML builder instance was specified
|
608
|
+
# then we assume that has been called recursively to generate a larger
|
609
|
+
# XML document.
|
610
|
+
write_out_to_xml ? builder.to_xml : builder
|
611
|
+
|
164
612
|
end
|
613
|
+
|
614
|
+
|
165
615
|
end
|
166
616
|
|
167
617
|
require 'happymapper/item'
|