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.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +64 -5
  3. data/README.md +296 -192
  4. data/lib/happymapper/anonymous_mapper.rb +46 -43
  5. data/lib/happymapper/attribute.rb +7 -5
  6. data/lib/happymapper/element.rb +19 -22
  7. data/lib/happymapper/item.rb +19 -21
  8. data/lib/happymapper/supported_types.rb +20 -28
  9. data/lib/happymapper/text_node.rb +4 -3
  10. data/lib/happymapper/version.rb +3 -1
  11. data/lib/happymapper.rb +336 -362
  12. data/lib/nokogiri-happymapper.rb +4 -0
  13. metadata +124 -105
  14. data/spec/attribute_default_value_spec.rb +0 -50
  15. data/spec/attributes_spec.rb +0 -36
  16. data/spec/fixtures/address.xml +0 -9
  17. data/spec/fixtures/ambigous_items.xml +0 -22
  18. data/spec/fixtures/analytics.xml +0 -61
  19. data/spec/fixtures/analytics_profile.xml +0 -127
  20. data/spec/fixtures/atom.xml +0 -19
  21. data/spec/fixtures/commit.xml +0 -52
  22. data/spec/fixtures/current_weather.xml +0 -89
  23. data/spec/fixtures/current_weather_missing_elements.xml +0 -18
  24. data/spec/fixtures/default_namespace_combi.xml +0 -6
  25. data/spec/fixtures/dictionary.xml +0 -20
  26. data/spec/fixtures/family_tree.xml +0 -21
  27. data/spec/fixtures/inagy.xml +0 -85
  28. data/spec/fixtures/lastfm.xml +0 -355
  29. data/spec/fixtures/multiple_namespaces.xml +0 -170
  30. data/spec/fixtures/multiple_primitives.xml +0 -5
  31. data/spec/fixtures/optional_attributes.xml +0 -6
  32. data/spec/fixtures/pita.xml +0 -133
  33. data/spec/fixtures/posts.xml +0 -23
  34. data/spec/fixtures/product_default_namespace.xml +0 -18
  35. data/spec/fixtures/product_no_namespace.xml +0 -10
  36. data/spec/fixtures/product_single_namespace.xml +0 -10
  37. data/spec/fixtures/quarters.xml +0 -19
  38. data/spec/fixtures/radar.xml +0 -21
  39. data/spec/fixtures/set_config_options.xml +0 -3
  40. data/spec/fixtures/statuses.xml +0 -422
  41. data/spec/fixtures/subclass_namespace.xml +0 -50
  42. data/spec/fixtures/unformatted_address.xml +0 -1
  43. data/spec/fixtures/wrapper.xml +0 -11
  44. data/spec/happymapper/attribute_spec.rb +0 -12
  45. data/spec/happymapper/element_spec.rb +0 -9
  46. data/spec/happymapper/item_spec.rb +0 -136
  47. data/spec/happymapper/text_node_spec.rb +0 -9
  48. data/spec/happymapper_parse_spec.rb +0 -113
  49. data/spec/happymapper_spec.rb +0 -1129
  50. data/spec/has_many_empty_array_spec.rb +0 -43
  51. data/spec/ignay_spec.rb +0 -95
  52. data/spec/inheritance_spec.rb +0 -107
  53. data/spec/mixed_namespaces_spec.rb +0 -61
  54. data/spec/parse_with_object_to_update_spec.rb +0 -111
  55. data/spec/spec_helper.rb +0 -7
  56. data/spec/to_xml_spec.rb +0 -201
  57. data/spec/to_xml_with_namespaces_spec.rb +0 -232
  58. data/spec/wilcard_tag_name_spec.rb +0 -96
  59. data/spec/wrap_spec.rb +0 -82
  60. 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
- extend AnonymousMapper
12
+ class XmlContent; end
11
13
 
12
- DEFAULT_NS = "happymapper"
14
+ def self.parse(xml_content)
15
+ AnonymousMapper.new.parse(xml_content)
16
+ end
13
17
 
14
18
  def self.included(base)
15
- if !(base.superclass <= HappyMapper)
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
- superclass.instance_variable_get(:@attributes).dup
22
+ superclass.instance_variable_get(:@attributes).dup
26
23
  @elements =
27
- superclass.instance_variable_get(:@elements).dup
24
+ superclass.instance_variable_get(:@elements).dup
28
25
  @registered_namespaces =
29
- superclass.instance_variable_get(:@registered_namespaces).dup
26
+ superclass.instance_variable_get(:@registered_namespaces).dup
30
27
  @wrapper_anonymous_classes =
31
- superclass.instance_variable_get(:@wrapper_anonymous_classes).dup
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] namespace the xml prefix
86
- # @param [String] ns url for the xml namespace
88
+ # @param [String] name the xml prefix
89
+ # @param [String] href url for the xml namespace
87
90
  #
88
- def register_namespace(namespace, ns)
89
- @registered_namespaces.merge!({namespace => ns})
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, {:single => true}.merge(options)
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, {:single => false}.merge(options)
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
- @wrapper_anonymous_classes[wrapper.inspect] = wrapper
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
- class_eval %{
250
- def #{item.method_name}
251
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
252
- @#{name}.#{item.method_name}
253
- end
254
- def #{item.method_name}=(value)
255
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
256
- @#{name}.#{item.method_name} = value
257
- end
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 calling Nokogiri::XML::Document.parse().
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. :single => true
283
- # if requesting a single object, otherwise it defaults to retuning an
284
- # array of multiple items. :xpath information where to start the parsing
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
- # create a local copy of the objects namespace value for this parse execution
290
- namespace = (@namespace if defined? @namespace)
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
- # If xml is an XML document select the root node of the document
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
- # 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)
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 set the default namespace to the one defined under 'xmlns'
329
- # or set the default namespace to the namespace that matches 'happymapper's
330
-
331
- if options[:namespace]
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 ( ( options[:single] || root ) ? nil : [] ) if nodes.size == 0
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
- # If an existing HappyMapper object is provided, update it with the
413
- # values from the xml being parsed. Otherwise, create a new object
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
- obj = options[:update] ? options[:update] : new
373
+ if block_given? && options[:in_groups_of]
374
+ yield part
375
+ else
376
+ collection += part
377
+ end
378
+ end
416
379
 
417
- attributes.each do |attr|
418
- value = attr.from_xml_node(n, namespace, namespaces)
419
- value = attr.default if value.nil?
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
- elements.each do |elem|
424
- obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
425
- end
384
+ if single
385
+ collection.first
386
+ else
387
+ collection
388
+ end
389
+ end
426
390
 
427
- if (defined? @content) && @content
428
- obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces))
429
- end
391
+ # @private
392
+ def defined_content
393
+ @content if defined? @content
394
+ end
430
395
 
431
- # If the HappyMapper class has the method #xml_value=,
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
- if obj.respond_to?('xml_value=')
436
- n.namespaces.each {|name,path| n[name] = path }
437
- obj.xml_value = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
438
- end
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
- # If the HappyMapper class has the method #xml_content=,
441
- # attr_write :xml_content, or attr_accessor :xml_content then we want to
442
- # assign the child xml that we just parsed to the xml_content
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
- if obj.respond_to?('xml_content=')
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
414
+ xpath += "#{namespace}:"
415
+ end
448
416
 
449
- # Call any registered after_parse callbacks for the object's class
417
+ nodes = []
450
418
 
451
- obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
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
- # collect the object that we have created
424
+ # If a tag has been provided we need to search for it.
454
425
 
455
- obj
456
- end
426
+ if options.key?(:tag)
427
+ nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
428
+ else
457
429
 
458
- # If a block has been provided and the user has requested that the objects
459
- # be handled in groups then we should yield the slice of the objects to them
460
- # otherwise continue to lump them together
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
- if block_given? and options[:in_groups_of]
463
- yield part
464
- else
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
- # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
471
- nodes = nil
441
+ nodes
442
+ end
472
443
 
473
- # If the :single option has been specified or we are at the root element
474
- # then we are going to return the first item in the collection. Otherwise
475
- # the return response is going to be an entire array of items.
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
- if options[:single] or root
478
- collection.first
479
- else
480
- collection
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
- # Find the attributes for the class and collect them into an array
532
- # that will be placed into a Hash structure
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
- attributes = self.class.attributes.collect do |attribute|
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
- builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_for_parent }
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.instance_variable_defined?('@content')
621
- if content = self.class.instance_variable_get('@content')
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
- end
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!(:update => self))
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
- def self.get(name, &blk)
762
- Class.new do
763
- include HappyMapper
764
- tag name
765
- instance_eval(&blk)
766
- end
767
- end
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'