nokogiri-happymapper 0.6.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|