nokogiri-happymapper 0.6.0 → 0.7.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.
@@ -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'