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