nokogiri-happymapper 0.6.0 → 0.7.0
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +26 -1
- data/README.md +204 -117
- data/lib/happymapper.rb +318 -343
- data/lib/happymapper/anonymous_mapper.rb +27 -29
- data/lib/happymapper/attribute.rb +7 -5
- data/lib/happymapper/element.rb +19 -24
- data/lib/happymapper/item.rb +18 -20
- data/lib/happymapper/supported_types.rb +20 -19
- data/lib/happymapper/text_node.rb +4 -3
- data/lib/happymapper/version.rb +3 -1
- data/spec/attribute_default_value_spec.rb +14 -15
- data/spec/attributes_spec.rb +14 -15
- data/spec/happymapper/attribute_spec.rb +4 -4
- data/spec/happymapper/element_spec.rb +3 -1
- data/spec/happymapper/item_spec.rb +49 -41
- data/spec/happymapper/text_node_spec.rb +3 -1
- data/spec/happymapper_parse_spec.rb +62 -44
- data/spec/happymapper_spec.rb +270 -263
- data/spec/has_many_empty_array_spec.rb +8 -7
- data/spec/ignay_spec.rb +27 -31
- data/spec/inheritance_spec.rb +30 -24
- data/spec/mixed_namespaces_spec.rb +14 -15
- data/spec/parse_with_object_to_update_spec.rb +37 -38
- data/spec/spec_helper.rb +18 -0
- data/spec/to_xml_spec.rb +64 -63
- data/spec/to_xml_with_namespaces_spec.rb +66 -64
- data/spec/wilcard_tag_name_spec.rb +25 -21
- data/spec/wrap_spec.rb +11 -11
- data/spec/xpath_spec.rb +33 -32
- metadata +33 -5
data/lib/happymapper.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'nokogiri'
|
2
4
|
require 'date'
|
3
5
|
require 'time'
|
6
|
+
require 'happymapper/version'
|
4
7
|
require 'happymapper/anonymous_mapper'
|
5
8
|
|
6
9
|
module HappyMapper
|
@@ -9,26 +12,24 @@ module HappyMapper
|
|
9
12
|
|
10
13
|
extend AnonymousMapper
|
11
14
|
|
12
|
-
DEFAULT_NS = "happymapper"
|
13
|
-
|
14
15
|
def self.included(base)
|
15
|
-
if
|
16
|
-
base.instance_eval do
|
17
|
-
@attributes = {}
|
18
|
-
@elements = {}
|
19
|
-
@registered_namespaces = {}
|
20
|
-
@wrapper_anonymous_classes = {}
|
21
|
-
end
|
22
|
-
else
|
16
|
+
if base.superclass <= HappyMapper
|
23
17
|
base.instance_eval do
|
24
18
|
@attributes =
|
25
|
-
|
19
|
+
superclass.instance_variable_get(:@attributes).dup
|
26
20
|
@elements =
|
27
|
-
|
21
|
+
superclass.instance_variable_get(:@elements).dup
|
28
22
|
@registered_namespaces =
|
29
|
-
|
23
|
+
superclass.instance_variable_get(:@registered_namespaces).dup
|
30
24
|
@wrapper_anonymous_classes =
|
31
|
-
|
25
|
+
superclass.instance_variable_get(:@wrapper_anonymous_classes).dup
|
26
|
+
end
|
27
|
+
else
|
28
|
+
base.instance_eval do
|
29
|
+
@attributes = {}
|
30
|
+
@elements = {}
|
31
|
+
@registered_namespaces = {}
|
32
|
+
@wrapper_anonymous_classes = {}
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
@@ -36,7 +37,6 @@ module HappyMapper
|
|
36
37
|
end
|
37
38
|
|
38
39
|
module ClassMethods
|
39
|
-
|
40
40
|
#
|
41
41
|
# The xml has the following attributes defined.
|
42
42
|
#
|
@@ -52,7 +52,7 @@ module HappyMapper
|
|
52
52
|
# the object will be converted upon parsing
|
53
53
|
# @param [Hash] options additional parameters to send to the relationship
|
54
54
|
#
|
55
|
-
def attribute(name, type, options={})
|
55
|
+
def attribute(name, type, options = {})
|
56
56
|
attribute = Attribute.new(name, type, options)
|
57
57
|
@attributes[name] = attribute
|
58
58
|
attr_accessor attribute.method_name.intern
|
@@ -82,11 +82,11 @@ module HappyMapper
|
|
82
82
|
# ...
|
83
83
|
# </outputXML>"
|
84
84
|
#
|
85
|
-
# @param [String]
|
86
|
-
# @param [String]
|
85
|
+
# @param [String] name the xml prefix
|
86
|
+
# @param [String] href url for the xml namespace
|
87
87
|
#
|
88
|
-
def register_namespace(
|
89
|
-
@registered_namespaces.merge!(
|
88
|
+
def register_namespace(name, href)
|
89
|
+
@registered_namespaces.merge!(name => href)
|
90
90
|
end
|
91
91
|
|
92
92
|
#
|
@@ -107,7 +107,7 @@ module HappyMapper
|
|
107
107
|
# the object will be converted upon parsing
|
108
108
|
# @param [Hash] options additional parameters to send to the relationship
|
109
109
|
#
|
110
|
-
def element(name, type, options={})
|
110
|
+
def element(name, type, options = {})
|
111
111
|
element = Element.new(name, type, options)
|
112
112
|
attr_accessor element.method_name.intern unless @elements[name]
|
113
113
|
@elements[name] = element
|
@@ -140,7 +140,7 @@ module HappyMapper
|
|
140
140
|
# the object will be converted upon parsing. By Default String class will be taken.
|
141
141
|
# @param [Hash] options additional parameters to send to the relationship
|
142
142
|
#
|
143
|
-
def content(name, type=String, options={})
|
143
|
+
def content(name, type = String, options = {})
|
144
144
|
@content = TextNode.new(name, type, options)
|
145
145
|
attr_accessor @content.method_name.intern
|
146
146
|
end
|
@@ -166,8 +166,8 @@ module HappyMapper
|
|
166
166
|
#
|
167
167
|
# @see #element
|
168
168
|
#
|
169
|
-
def has_one(name, type, options={})
|
170
|
-
element name, type, {:
|
169
|
+
def has_one(name, type, options = {})
|
170
|
+
element name, type, { single: true }.merge(options)
|
171
171
|
end
|
172
172
|
|
173
173
|
#
|
@@ -180,8 +180,8 @@ module HappyMapper
|
|
180
180
|
#
|
181
181
|
# @see #element
|
182
182
|
#
|
183
|
-
def has_many(name, type, options={})
|
184
|
-
element name, type, {:
|
183
|
+
def has_many(name, type, options = {})
|
184
|
+
element name, type, { single: false }.merge(options)
|
185
185
|
end
|
186
186
|
|
187
187
|
#
|
@@ -226,7 +226,7 @@ module HappyMapper
|
|
226
226
|
# @return [String] the name of the tag as a string, downcased
|
227
227
|
#
|
228
228
|
def tag_name
|
229
|
-
@tag_name ||= to_s.split('::')[-1].downcase
|
229
|
+
@tag_name ||= name && name.to_s.split('::')[-1].downcase
|
230
230
|
end
|
231
231
|
|
232
232
|
# There is an XML tag that needs to be known for parsing and should be generated
|
@@ -240,22 +240,23 @@ module HappyMapper
|
|
240
240
|
# Get an anonymous HappyMapper that has 'name' as its tag and defined
|
241
241
|
# in '&blk'. Then save that to a class instance variable for later use
|
242
242
|
wrapper = AnonymousWrapperClassFactory.get(name, &blk)
|
243
|
-
|
243
|
+
wrapper_key = wrapper.inspect
|
244
|
+
@wrapper_anonymous_classes[wrapper_key] = wrapper
|
244
245
|
|
245
246
|
# Create getter/setter for each element and attribute defined on the anonymous HappyMapper
|
246
247
|
# onto this class. They get/set the value by passing thru to the anonymous class.
|
247
248
|
passthrus = wrapper.attributes + wrapper.elements
|
248
249
|
passthrus.each do |item|
|
249
|
-
class_eval
|
250
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
250
251
|
def #{item.method_name}
|
251
|
-
@#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{
|
252
|
+
@#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
|
252
253
|
@#{name}.#{item.method_name}
|
253
254
|
end
|
254
255
|
def #{item.method_name}=(value)
|
255
|
-
@#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{
|
256
|
+
@#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
|
256
257
|
@#{name}.#{item.method_name} = value
|
257
258
|
end
|
258
|
-
|
259
|
+
RUBY
|
259
260
|
end
|
260
261
|
|
261
262
|
has_one name, wrapper
|
@@ -267,7 +268,9 @@ module HappyMapper
|
|
267
268
|
#
|
268
269
|
attr_reader :nokogiri_config_callback
|
269
270
|
|
270
|
-
# Register a config callback according to the block Nokogori expects when
|
271
|
+
# Register a config callback according to the block Nokogori expects when
|
272
|
+
# calling Nokogiri::XML::Document.parse().
|
273
|
+
#
|
271
274
|
# See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
|
272
275
|
#
|
273
276
|
# @param [Proc] the proc to pass to Nokogiri to setup parse options
|
@@ -285,20 +288,20 @@ module HappyMapper
|
|
285
288
|
# :namespace is the namespace to use for additional information.
|
286
289
|
#
|
287
290
|
def parse(xml, options = {})
|
288
|
-
|
289
291
|
# create a local copy of the objects namespace value for this parse execution
|
290
292
|
namespace = (@namespace if defined? @namespace)
|
291
293
|
|
294
|
+
# Capture any provided namespaces and merge in any namespaces that have
|
295
|
+
# been registered on the object.
|
296
|
+
namespaces = options[:namespaces] || {}
|
297
|
+
namespaces = namespaces.merge(@registered_namespaces)
|
298
|
+
|
292
299
|
# If the XML specified is an Node then we have what we need.
|
293
300
|
if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
|
294
301
|
node = xml
|
295
302
|
else
|
296
303
|
|
297
|
-
|
298
|
-
if xml.is_a?(Nokogiri::XML::Document)
|
299
|
-
node = xml.root
|
300
|
-
else
|
301
|
-
|
304
|
+
unless xml.is_a?(Nokogiri::XML::Document)
|
302
305
|
# Attempt to parse the xml value with Nokogiri XML as a document
|
303
306
|
# and select the root element
|
304
307
|
xml = Nokogiri::XML(
|
@@ -306,8 +309,12 @@ module HappyMapper
|
|
306
309
|
Nokogiri::XML::ParseOptions::STRICT,
|
307
310
|
&nokogiri_config_callback
|
308
311
|
)
|
309
|
-
node = xml.root
|
310
312
|
end
|
313
|
+
# Now xml is certainly an XML document: Select the root node of the document
|
314
|
+
node = xml.root
|
315
|
+
|
316
|
+
# merge any namespaces found on the xml node into the namespace hash
|
317
|
+
namespaces = namespaces.merge(xml.collect_namespaces)
|
311
318
|
|
312
319
|
# if the node name is equal to the tag name then the we are parsing the
|
313
320
|
# root element and that is important to record so that we can apply
|
@@ -316,81 +323,25 @@ module HappyMapper
|
|
316
323
|
root = node.name == tag_name
|
317
324
|
end
|
318
325
|
|
319
|
-
# if any namespaces have been provied then we should capture those and then
|
320
|
-
# merge them with any namespaces found on the xml node and merge all that
|
321
|
-
# with any namespaces that have been registered on the object
|
322
|
-
|
323
|
-
namespaces = options[:namespaces] || {}
|
324
|
-
namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
|
325
|
-
namespaces = namespaces.merge(@registered_namespaces)
|
326
|
-
|
327
326
|
# if a namespace has been provided then set the current namespace to it
|
328
327
|
# or set the default namespace to the one defined under 'xmlns'
|
329
328
|
# or set the default namespace to the namespace that matches 'happymapper's
|
330
329
|
|
331
330
|
if options[:namespace]
|
332
331
|
namespace = options[:namespace]
|
333
|
-
elsif namespaces.has_key?(
|
334
|
-
namespace ||=
|
335
|
-
namespaces[DEFAULT_NS] = namespaces.delete("xmlns")
|
336
|
-
elsif namespaces.has_key?(DEFAULT_NS)
|
337
|
-
namespace ||= DEFAULT_NS
|
332
|
+
elsif namespaces.has_key?('xmlns')
|
333
|
+
namespace ||= 'xmlns'
|
338
334
|
end
|
339
335
|
|
340
336
|
# from the options grab any nodes present and if none are present then
|
341
337
|
# perform the following to find the nodes for the given class
|
342
338
|
|
343
339
|
nodes = options.fetch(:nodes) do
|
344
|
-
|
345
|
-
# when at the root use the xpath '/' otherwise use a more gready './/'
|
346
|
-
# unless an xpath has been specified, which should overwrite default
|
347
|
-
# and finally attach the current namespace if one has been defined
|
348
|
-
#
|
349
|
-
|
350
|
-
xpath = (root ? '/' : './/')
|
351
|
-
xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
|
352
|
-
xpath += "#{namespace}:" if namespace
|
353
|
-
|
354
|
-
nodes = []
|
355
|
-
|
356
|
-
# when finding nodes, do it in this order:
|
357
|
-
# 1. specified tag if one has been provided
|
358
|
-
# 2. name of element
|
359
|
-
# 3. tag_name (derived from class name by default)
|
360
|
-
|
361
|
-
# If a tag has been provided we need to search for it.
|
362
|
-
|
363
|
-
if options.key?(:tag)
|
364
|
-
begin
|
365
|
-
nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
|
366
|
-
rescue
|
367
|
-
# This exception takes place when the namespace is often not found
|
368
|
-
# and we should continue on with the empty array of nodes.
|
369
|
-
end
|
370
|
-
else
|
371
|
-
|
372
|
-
# This is the default case when no tag value is provided.
|
373
|
-
# First we use the name of the element `items` in `has_many items`
|
374
|
-
# Second we use the tag name which is the name of the class cleaned up
|
375
|
-
|
376
|
-
[options[:name], tag_name].compact.each do |xpath_ext|
|
377
|
-
begin
|
378
|
-
nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
|
379
|
-
rescue
|
380
|
-
break
|
381
|
-
# This exception takes place when the namespace is often not found
|
382
|
-
# and we should continue with the empty array of nodes or keep looking
|
383
|
-
end
|
384
|
-
break if nodes && !nodes.empty?
|
385
|
-
end
|
386
|
-
|
387
|
-
end
|
388
|
-
|
389
|
-
nodes
|
340
|
+
find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
|
390
341
|
end
|
391
342
|
|
392
343
|
# Nothing matching found, we can go ahead and return
|
393
|
-
return (
|
344
|
+
return (options[:single] || root ? nil : []) if nodes.size == 0
|
394
345
|
|
395
346
|
# If the :limit option has been specified then we are going to slice
|
396
347
|
# our node results by that amount to allow us the ability to deal with
|
@@ -406,86 +357,130 @@ module HappyMapper
|
|
406
357
|
collection = []
|
407
358
|
|
408
359
|
nodes.each_slice(limit) do |slice|
|
409
|
-
|
410
360
|
part = slice.map do |n|
|
361
|
+
parse_node(n, options, namespace, namespaces)
|
362
|
+
end
|
411
363
|
|
412
|
-
|
413
|
-
|
364
|
+
# If a block has been provided and the user has requested that the objects
|
365
|
+
# be handled in groups then we should yield the slice of the objects to them
|
366
|
+
# otherwise continue to lump them together
|
414
367
|
|
415
|
-
|
368
|
+
if block_given? && options[:in_groups_of]
|
369
|
+
yield part
|
370
|
+
else
|
371
|
+
collection += part
|
372
|
+
end
|
373
|
+
end
|
416
374
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
obj.send("#{attr.method_name}=", value)
|
421
|
-
end
|
375
|
+
# If the :single option has been specified or we are at the root element
|
376
|
+
# then we are going to return the first item in the collection. Otherwise
|
377
|
+
# the return response is going to be an entire array of items.
|
422
378
|
|
423
|
-
|
424
|
-
|
425
|
-
|
379
|
+
if options[:single] || root
|
380
|
+
collection.first
|
381
|
+
else
|
382
|
+
collection
|
383
|
+
end
|
384
|
+
end
|
426
385
|
|
427
|
-
|
428
|
-
|
429
|
-
|
386
|
+
# @private
|
387
|
+
def defined_content
|
388
|
+
@content if defined? @content
|
389
|
+
end
|
430
390
|
|
431
|
-
|
432
|
-
# attr_writer :xml_value, or attr_accessor :xml_value then we want to
|
433
|
-
# assign the current xml that we just parsed to the xml_value
|
391
|
+
private
|
434
392
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
393
|
+
def find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
|
394
|
+
# when at the root use the xpath '/' otherwise use a more gready './/'
|
395
|
+
# unless an xpath has been specified, which should overwrite default
|
396
|
+
# and finally attach the current namespace if one has been defined
|
397
|
+
#
|
439
398
|
|
440
|
-
|
441
|
-
|
442
|
-
|
399
|
+
xpath = (root ? '/' : './/')
|
400
|
+
xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
|
401
|
+
if namespace
|
402
|
+
return [] unless namespaces.find { |name, _url| ["xmlns:#{namespace}", namespace].include? name }
|
403
|
+
xpath += "#{namespace}:"
|
404
|
+
end
|
443
405
|
|
444
|
-
|
445
|
-
n = n.children if n.respond_to?(:children)
|
446
|
-
obj.xml_content = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
447
|
-
end
|
406
|
+
nodes = []
|
448
407
|
|
449
|
-
|
408
|
+
# when finding nodes, do it in this order:
|
409
|
+
# 1. specified tag if one has been provided
|
410
|
+
# 2. name of element
|
411
|
+
# 3. tag_name (derived from class name by default)
|
450
412
|
|
451
|
-
|
413
|
+
# If a tag has been provided we need to search for it.
|
414
|
+
|
415
|
+
if options.key?(:tag)
|
416
|
+
nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
|
417
|
+
else
|
452
418
|
|
453
|
-
|
419
|
+
# This is the default case when no tag value is provided.
|
420
|
+
# First we use the name of the element `items` in `has_many items`
|
421
|
+
# Second we use the tag name which is the name of the class cleaned up
|
454
422
|
|
455
|
-
|
423
|
+
[options[:name], tag_name].compact.each do |xpath_ext|
|
424
|
+
nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
|
425
|
+
break if nodes && !nodes.empty?
|
456
426
|
end
|
457
427
|
|
458
|
-
|
459
|
-
# be handled in groups then we should yield the slice of the objects to them
|
460
|
-
# otherwise continue to lump them together
|
428
|
+
end
|
461
429
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
430
|
+
nodes
|
431
|
+
end
|
432
|
+
|
433
|
+
def parse_node(node, options, namespace, namespaces)
|
434
|
+
# If an existing HappyMapper object is provided, update it with the
|
435
|
+
# values from the xml being parsed. Otherwise, create a new object
|
467
436
|
|
437
|
+
obj = options[:update] || new
|
438
|
+
|
439
|
+
attributes.each do |attr|
|
440
|
+
value = attr.from_xml_node(node, namespace, namespaces)
|
441
|
+
value = attr.default if value.nil?
|
442
|
+
obj.send("#{attr.method_name}=", value)
|
468
443
|
end
|
469
444
|
|
470
|
-
|
471
|
-
|
445
|
+
elements.each do |elem|
|
446
|
+
obj.send("#{elem.method_name}=", elem.from_xml_node(node, namespace, namespaces))
|
447
|
+
end
|
472
448
|
|
473
|
-
|
474
|
-
|
475
|
-
|
449
|
+
if (content = defined_content)
|
450
|
+
obj.send("#{content.method_name}=", content.from_xml_node(node, namespace, namespaces))
|
451
|
+
end
|
476
452
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
453
|
+
# If the HappyMapper class has the method #xml_value=,
|
454
|
+
# attr_writer :xml_value, or attr_accessor :xml_value then we want to
|
455
|
+
# assign the current xml that we just parsed to the xml_value
|
456
|
+
|
457
|
+
if obj.respond_to?('xml_value=')
|
458
|
+
obj.xml_value = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
481
459
|
end
|
460
|
+
|
461
|
+
# If the HappyMapper class has the method #xml_content=,
|
462
|
+
# attr_write :xml_content, or attr_accessor :xml_content then we want to
|
463
|
+
# assign the child xml that we just parsed to the xml_content
|
464
|
+
|
465
|
+
if obj.respond_to?('xml_content=')
|
466
|
+
node = node.children if node.respond_to?(:children)
|
467
|
+
obj.xml_content = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
|
468
|
+
end
|
469
|
+
|
470
|
+
# Call any registered after_parse callbacks for the object's class
|
471
|
+
|
472
|
+
obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
|
473
|
+
|
474
|
+
# collect the object that we have created
|
475
|
+
|
476
|
+
obj
|
482
477
|
end
|
483
478
|
end
|
484
479
|
|
485
480
|
# Set all attributes with a default to their default values
|
486
481
|
def initialize
|
487
482
|
super
|
488
|
-
self.class.attributes.reject {|attr| attr.default.nil?}.each do |attr|
|
483
|
+
self.class.attributes.reject { |attr| attr.default.nil? }.each do |attr|
|
489
484
|
send("#{attr.method_name}=", attr.default)
|
490
485
|
end
|
491
486
|
end
|
@@ -519,6 +514,7 @@ module HappyMapper
|
|
519
514
|
#
|
520
515
|
# If to_xml has been called without a passed in builder instance that
|
521
516
|
# means we are going to return xml output. When it has been called with
|
517
|
+
|
522
518
|
# a builder instance that means we most likely being called recursively
|
523
519
|
# and will return the end product as a builder instance.
|
524
520
|
#
|
@@ -527,34 +523,124 @@ module HappyMapper
|
|
527
523
|
builder = Nokogiri::XML::Builder.new
|
528
524
|
end
|
529
525
|
|
526
|
+
attributes = collect_writable_attributes
|
527
|
+
|
528
|
+
#
|
529
|
+
# If the object we are serializing has a namespace declaration we will want
|
530
|
+
# to use that namespace or we will use the default namespace.
|
531
|
+
# When neither are specifed we are simply using whatever is default to the
|
532
|
+
# builder
|
533
|
+
#
|
534
|
+
namespace_name = namespace_override || self.class.namespace || default_namespace
|
535
|
+
|
536
|
+
#
|
537
|
+
# Create a tag in the builder that matches the class's tag name unless a tag was passed
|
538
|
+
# in a recursive call from the parent doc. Then append
|
539
|
+
# any attributes to the element that were defined above.
|
540
|
+
#
|
541
|
+
builder.send("#{tag_from_parent || self.class.tag_name}_", attributes) do |xml|
|
542
|
+
register_namespaces_with_builder(builder)
|
543
|
+
|
544
|
+
xml.parent.namespace =
|
545
|
+
builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_name }
|
546
|
+
|
547
|
+
#
|
548
|
+
# When a content has been defined we add the resulting value
|
549
|
+
# the output xml
|
550
|
+
#
|
551
|
+
if (content = self.class.defined_content)
|
552
|
+
|
553
|
+
unless content.options[:read_only]
|
554
|
+
value = send(content.name)
|
555
|
+
value = apply_on_save_action(content, value)
|
556
|
+
|
557
|
+
builder.text(value)
|
558
|
+
end
|
559
|
+
|
560
|
+
end
|
561
|
+
|
562
|
+
#
|
563
|
+
# for every define element (i.e. has_one, has_many, element) we are
|
564
|
+
# going to persist each one
|
565
|
+
#
|
566
|
+
self.class.elements.each do |element|
|
567
|
+
element_to_xml(element, xml, default_namespace)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
# Write out to XML, this value was set above, based on whether or not an XML
|
572
|
+
# builder object was passed to it as a parameter. When there was no parameter
|
573
|
+
# we assume we are at the root level of the #to_xml call and want the actual
|
574
|
+
# xml generated from the object. If an XML builder instance was specified
|
575
|
+
# then we assume that has been called recursively to generate a larger
|
576
|
+
# XML document.
|
577
|
+
write_out_to_xml ? builder.to_xml.force_encoding('UTF-8') : builder
|
578
|
+
end
|
579
|
+
|
580
|
+
# Parse the xml and update this instance. This does not update instances
|
581
|
+
# of HappyMappers that are children of this object. New instances will be
|
582
|
+
# created for any HappyMapper children of this object.
|
583
|
+
#
|
584
|
+
# Params and return are the same as the class parse() method above.
|
585
|
+
def parse(xml, options = {})
|
586
|
+
self.class.parse(xml, options.merge!(update: self))
|
587
|
+
end
|
588
|
+
|
589
|
+
# Factory for creating anonmyous HappyMappers
|
590
|
+
class AnonymousWrapperClassFactory
|
591
|
+
def self.get(name, &blk)
|
592
|
+
Class.new do
|
593
|
+
include HappyMapper
|
594
|
+
tag name
|
595
|
+
instance_eval(&blk)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
private
|
601
|
+
|
602
|
+
#
|
603
|
+
# If the item defines an on_save lambda/proc or value that maps to a method
|
604
|
+
# that the class has defined, then call it with the value as a parameter.
|
605
|
+
# This allows for operations to be performed to convert the value to a
|
606
|
+
# specific value to be saved to the xml.
|
607
|
+
#
|
608
|
+
def apply_on_save_action(item, value)
|
609
|
+
if (on_save_action = item.options[:on_save])
|
610
|
+
if on_save_action.is_a?(Proc)
|
611
|
+
value = on_save_action.call(value)
|
612
|
+
elsif respond_to?(on_save_action)
|
613
|
+
value = send(on_save_action, value)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
value
|
617
|
+
end
|
618
|
+
|
619
|
+
#
|
620
|
+
# Find the attributes for the class and collect them into a Hash structure
|
621
|
+
#
|
622
|
+
def collect_writable_attributes
|
530
623
|
#
|
531
624
|
# Find the attributes for the class and collect them into an array
|
532
625
|
# that will be placed into a Hash structure
|
533
626
|
#
|
534
627
|
attributes = self.class.attributes.collect do |attribute|
|
535
|
-
|
536
628
|
#
|
537
629
|
# If an attribute is marked as read_only then we want to ignore the attribute
|
538
|
-
# when it comes to saving the xml document; so we
|
630
|
+
# when it comes to saving the xml document; so we will not go into any of
|
539
631
|
# the below process
|
540
632
|
#
|
541
|
-
|
633
|
+
if attribute.options[:read_only]
|
634
|
+
[]
|
635
|
+
else
|
542
636
|
|
543
637
|
value = send(attribute.method_name)
|
544
638
|
value = nil if value == attribute.default
|
545
639
|
|
546
640
|
#
|
547
|
-
#
|
548
|
-
# a method that the class has defined, then call it with the value as a
|
549
|
-
# parameter.
|
641
|
+
# Apply any on_save lambda/proc or value defined on the attribute.
|
550
642
|
#
|
551
|
-
|
552
|
-
if on_save_action.is_a?(Proc)
|
553
|
-
value = on_save_action.call(value)
|
554
|
-
elsif respond_to?(on_save_action)
|
555
|
-
value = send(on_save_action,value)
|
556
|
-
end
|
557
|
-
end
|
643
|
+
value = apply_on_save_action(attribute, value)
|
558
644
|
|
559
645
|
#
|
560
646
|
# Attributes that have a nil value should be ignored unless they explicitly
|
@@ -562,211 +648,100 @@ module HappyMapper
|
|
562
648
|
#
|
563
649
|
if not value.nil? || attribute.options[:state_when_nil]
|
564
650
|
attribute_namespace = attribute.options[:namespace]
|
565
|
-
[
|
651
|
+
["#{attribute_namespace ? "#{attribute_namespace}:" : ''}#{attribute.tag}", value]
|
566
652
|
else
|
567
653
|
[]
|
568
654
|
end
|
569
655
|
|
570
|
-
else
|
571
|
-
[]
|
572
656
|
end
|
573
|
-
|
574
657
|
end.flatten
|
575
658
|
|
576
|
-
|
659
|
+
Hash[*attributes]
|
660
|
+
end
|
577
661
|
|
662
|
+
#
|
663
|
+
# Add all the registered namespaces to the builder's root element.
|
664
|
+
# When this is called recursively by composed classes the namespaces
|
665
|
+
# are still added to the root element
|
666
|
+
#
|
667
|
+
# However, we do not want to add the namespace if the namespace is 'xmlns'
|
668
|
+
# which means that it is the default namespace of the code.
|
669
|
+
#
|
670
|
+
def register_namespaces_with_builder(builder)
|
671
|
+
return unless self.class.instance_variable_get('@registered_namespaces')
|
672
|
+
self.class.instance_variable_get('@registered_namespaces').sort.each do |name, href|
|
673
|
+
name = nil if name == 'xmlns'
|
674
|
+
builder.doc.root.add_namespace(name, href)
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
# Persist a single nested element as xml
|
679
|
+
def element_to_xml(element, xml, default_namespace)
|
578
680
|
#
|
579
|
-
#
|
580
|
-
#
|
581
|
-
# any attributes to the element that were defined above.
|
681
|
+
# If an element is marked as read only do not consider at all when
|
682
|
+
# saving to XML.
|
582
683
|
#
|
583
|
-
|
684
|
+
return if element.options[:read_only]
|
584
685
|
|
585
|
-
|
586
|
-
# Add all the registered namespaces to the root element.
|
587
|
-
# When this is called recurisvely by composed classes the namespaces
|
588
|
-
# are still added to the root element
|
589
|
-
#
|
590
|
-
# However, we do not want to add the namespace if the namespace is 'xmlns'
|
591
|
-
# which means that it is the default namesapce of the code.
|
592
|
-
#
|
593
|
-
if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
|
594
|
-
self.class.instance_variable_get('@registered_namespaces').each_pair do |name,href|
|
595
|
-
name = nil if name == "xmlns"
|
596
|
-
builder.doc.root.add_namespace(name,href)
|
597
|
-
end
|
598
|
-
end
|
686
|
+
tag = element.tag || element.name
|
599
687
|
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
namespace_for_parent = namespace_override
|
607
|
-
if self.class.respond_to?(:namespace) && self.class.namespace
|
608
|
-
namespace_for_parent ||= self.class.namespace
|
609
|
-
end
|
610
|
-
namespace_for_parent ||= default_namespace
|
611
|
-
|
612
|
-
xml.parent.namespace =
|
613
|
-
builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_for_parent }
|
688
|
+
#
|
689
|
+
# The value to store is the result of the method call to the element,
|
690
|
+
# by default this is simply utilizing the attr_accessor defined. However,
|
691
|
+
# this allows for this method to be overridden
|
692
|
+
#
|
693
|
+
value = send(element.name)
|
614
694
|
|
695
|
+
#
|
696
|
+
# Apply any on_save action defined on the element.
|
697
|
+
#
|
698
|
+
value = apply_on_save_action(element, value)
|
615
699
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
value = send(text_accessor)
|
626
|
-
|
627
|
-
if on_save_action = content.options[:on_save]
|
628
|
-
if on_save_action.is_a?(Proc)
|
629
|
-
value = on_save_action.call(value)
|
630
|
-
elsif respond_to?(on_save_action)
|
631
|
-
value = send(on_save_action,value)
|
632
|
-
end
|
633
|
-
end
|
634
|
-
|
635
|
-
builder.text(value)
|
636
|
-
end
|
700
|
+
#
|
701
|
+
# To allow for us to treat both groups of items and singular items
|
702
|
+
# equally we wrap the value and treat it as an array.
|
703
|
+
#
|
704
|
+
values = if value.respond_to?(:to_ary) && !element.options[:single]
|
705
|
+
value.to_ary
|
706
|
+
else
|
707
|
+
[value]
|
708
|
+
end
|
637
709
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
#
|
642
|
-
# for every define element (i.e. has_one, has_many, element) we are
|
643
|
-
# going to persist each one
|
644
|
-
#
|
645
|
-
self.class.elements.each do |element|
|
710
|
+
values.each do |item|
|
711
|
+
if item.is_a?(HappyMapper)
|
646
712
|
|
647
713
|
#
|
648
|
-
#
|
649
|
-
#
|
714
|
+
# Other items are convertable to xml through the xml builder
|
715
|
+
# process should have their contents retrieved and attached
|
716
|
+
# to the builder structure
|
650
717
|
#
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
#
|
656
|
-
# The value to store is the result of the method call to the element,
|
657
|
-
# by default this is simply utilizing the attr_accessor defined. However,
|
658
|
-
# this allows for this method to be overridden
|
659
|
-
#
|
660
|
-
value = send(element.name)
|
661
|
-
|
662
|
-
#
|
663
|
-
# If the element defines an on_save lambda/proc then we will call that
|
664
|
-
# operation on the specified value. This allows for operations to be
|
665
|
-
# performed to convert the value to a specific value to be saved to the xml.
|
666
|
-
#
|
667
|
-
if on_save_action = element.options[:on_save]
|
668
|
-
if on_save_action.is_a?(Proc)
|
669
|
-
value = on_save_action.call(value)
|
670
|
-
elsif respond_to?(on_save_action)
|
671
|
-
value = send(on_save_action,value)
|
672
|
-
end
|
673
|
-
end
|
674
|
-
|
675
|
-
#
|
676
|
-
# Normally a nil value would be ignored, however if specified then
|
677
|
-
# an empty element will be written to the xml
|
678
|
-
#
|
679
|
-
if value.nil? && element.options[:single] && element.options[:state_when_nil]
|
680
|
-
xml.send("#{tag}_","")
|
681
|
-
end
|
682
|
-
|
683
|
-
#
|
684
|
-
# To allow for us to treat both groups of items and singular items
|
685
|
-
# equally we wrap the value and treat it as an array.
|
686
|
-
#
|
687
|
-
if value.nil?
|
688
|
-
values = []
|
689
|
-
elsif value.respond_to?(:to_ary) && !element.options[:single]
|
690
|
-
values = value.to_ary
|
691
|
-
else
|
692
|
-
values = [value]
|
693
|
-
end
|
718
|
+
item.to_xml(xml, self.class.namespace || default_namespace,
|
719
|
+
element.options[:namespace],
|
720
|
+
element.options[:tag] || nil)
|
694
721
|
|
695
|
-
|
722
|
+
elsif !item.nil?
|
696
723
|
|
697
|
-
|
724
|
+
item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
|
698
725
|
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
elsif !item.nil?
|
709
|
-
|
710
|
-
item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
|
711
|
-
|
712
|
-
#
|
713
|
-
# When a value exists we should append the value for the tag
|
714
|
-
#
|
715
|
-
if item_namespace
|
716
|
-
xml[item_namespace].send("#{tag}_",item.to_s)
|
717
|
-
else
|
718
|
-
xml.send("#{tag}_",item.to_s)
|
719
|
-
end
|
720
|
-
|
721
|
-
else
|
722
|
-
|
723
|
-
#
|
724
|
-
# Normally a nil value would be ignored, however if specified then
|
725
|
-
# an empty element will be written to the xml
|
726
|
-
#
|
727
|
-
xml.send("#{tag}_","") if element.options[:state_when_nil]
|
728
|
-
|
729
|
-
end
|
726
|
+
#
|
727
|
+
# When a value exists we should append the value for the tag
|
728
|
+
#
|
729
|
+
if item_namespace
|
730
|
+
xml[item_namespace].send("#{tag}_", item.to_s)
|
731
|
+
else
|
732
|
+
xml.send("#{tag}_", item.to_s)
|
733
|
+
end
|
730
734
|
|
731
|
-
|
735
|
+
elsif element.options[:state_when_nil]
|
732
736
|
|
733
|
-
|
737
|
+
#
|
738
|
+
# Normally a nil value would be ignored, however if specified then
|
739
|
+
# an empty element will be written to the xml
|
740
|
+
#
|
741
|
+
xml.send("#{tag}_", '')
|
734
742
|
end
|
735
|
-
|
736
743
|
end
|
737
|
-
|
738
|
-
# Write out to XML, this value was set above, based on whether or not an XML
|
739
|
-
# builder object was passed to it as a parameter. When there was no parameter
|
740
|
-
# we assume we are at the root level of the #to_xml call and want the actual
|
741
|
-
# xml generated from the object. If an XML builder instance was specified
|
742
|
-
# then we assume that has been called recursively to generate a larger
|
743
|
-
# XML document.
|
744
|
-
write_out_to_xml ? builder.to_xml : builder
|
745
|
-
|
746
744
|
end
|
747
|
-
|
748
|
-
# Parse the xml and update this instance. This does not update instances
|
749
|
-
# of HappyMappers that are children of this object. New instances will be
|
750
|
-
# created for any HappyMapper children of this object.
|
751
|
-
#
|
752
|
-
# Params and return are the same as the class parse() method above.
|
753
|
-
def parse(xml, options = {})
|
754
|
-
self.class.parse(xml, options.merge!(:update => self))
|
755
|
-
end
|
756
|
-
|
757
|
-
private
|
758
|
-
|
759
|
-
# Factory for creating anonmyous HappyMappers
|
760
|
-
class AnonymousWrapperClassFactory
|
761
|
-
def self.get(name, &blk)
|
762
|
-
Class.new do
|
763
|
-
include HappyMapper
|
764
|
-
tag name
|
765
|
-
instance_eval(&blk)
|
766
|
-
end
|
767
|
-
end
|
768
|
-
end
|
769
|
-
|
770
745
|
end
|
771
746
|
|
772
747
|
require 'happymapper/supported_types'
|