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.
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'