nokogiri-happymapper 0.6.0 → 0.7.0

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