blockscore-happymapper 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +605 -0
  4. data/lib/happymapper.rb +754 -0
  5. data/lib/happymapper/anonymous_mapper.rb +107 -0
  6. data/lib/happymapper/attribute.rb +21 -0
  7. data/lib/happymapper/element.rb +53 -0
  8. data/lib/happymapper/item.rb +156 -0
  9. data/lib/happymapper/supported_types.rb +137 -0
  10. data/lib/happymapper/text_node.rb +7 -0
  11. data/lib/happymapper/version.rb +3 -0
  12. data/spec/attribute_default_value_spec.rb +47 -0
  13. data/spec/attributes_spec.rb +33 -0
  14. data/spec/fixtures/address.xml +9 -0
  15. data/spec/fixtures/ambigous_items.xml +22 -0
  16. data/spec/fixtures/analytics.xml +61 -0
  17. data/spec/fixtures/analytics_profile.xml +127 -0
  18. data/spec/fixtures/atom.xml +19 -0
  19. data/spec/fixtures/commit.xml +52 -0
  20. data/spec/fixtures/current_weather.xml +89 -0
  21. data/spec/fixtures/current_weather_missing_elements.xml +18 -0
  22. data/spec/fixtures/default_namespace_combi.xml +6 -0
  23. data/spec/fixtures/dictionary.xml +20 -0
  24. data/spec/fixtures/family_tree.xml +21 -0
  25. data/spec/fixtures/inagy.xml +85 -0
  26. data/spec/fixtures/lastfm.xml +355 -0
  27. data/spec/fixtures/multiple_namespaces.xml +170 -0
  28. data/spec/fixtures/multiple_primitives.xml +5 -0
  29. data/spec/fixtures/optional_attributes.xml +6 -0
  30. data/spec/fixtures/pita.xml +133 -0
  31. data/spec/fixtures/posts.xml +23 -0
  32. data/spec/fixtures/product_default_namespace.xml +18 -0
  33. data/spec/fixtures/product_no_namespace.xml +10 -0
  34. data/spec/fixtures/product_single_namespace.xml +10 -0
  35. data/spec/fixtures/quarters.xml +19 -0
  36. data/spec/fixtures/radar.xml +21 -0
  37. data/spec/fixtures/set_config_options.xml +3 -0
  38. data/spec/fixtures/statuses.xml +422 -0
  39. data/spec/fixtures/subclass_namespace.xml +50 -0
  40. data/spec/fixtures/wrapper.xml +11 -0
  41. data/spec/happymapper/attribute_spec.rb +10 -0
  42. data/spec/happymapper/element_spec.rb +9 -0
  43. data/spec/happymapper/item_spec.rb +114 -0
  44. data/spec/happymapper/text_node_spec.rb +9 -0
  45. data/spec/happymapper_parse_spec.rb +99 -0
  46. data/spec/happymapper_spec.rb +1096 -0
  47. data/spec/has_many_empty_array_spec.rb +42 -0
  48. data/spec/ignay_spec.rb +87 -0
  49. data/spec/inheritance_spec.rb +105 -0
  50. data/spec/mixed_namespaces_spec.rb +58 -0
  51. data/spec/parse_with_object_to_update_spec.rb +108 -0
  52. data/spec/spec_helper.rb +8 -0
  53. data/spec/to_xml_spec.rb +197 -0
  54. data/spec/to_xml_with_namespaces_spec.rb +223 -0
  55. data/spec/wilcard_tag_name_spec.rb +94 -0
  56. data/spec/wrap_spec.rb +80 -0
  57. data/spec/xpath_spec.rb +86 -0
  58. metadata +183 -0
@@ -0,0 +1,754 @@
1
+ require 'nokogiri'
2
+ require 'date'
3
+ require 'time'
4
+ require 'happymapper/anonymous_mapper'
5
+
6
+ module HappyMapper
7
+ class Boolean; end
8
+ class XmlContent; end
9
+
10
+ extend AnonymousMapper
11
+
12
+ DEFAULT_NS = 'happymapper'
13
+
14
+ def self.included(base)
15
+ base.instance_eval do
16
+ @attributes = {}
17
+ @elements = {}
18
+ @registered_namespaces = {}
19
+ @wrapper_anonymous_classes = {}
20
+
21
+ if (base.superclass <= HappyMapper)
22
+ @attributes = superclass.instance_variable_get(:@attributes).dup unless superclass.instance_variable_get(:@attributes).nil?
23
+ @elements = superclass.instance_variable_get(:@elements).dup unless superclass.instance_variable_get(:@elements).nil?
24
+ @registered_namespaces = superclass.instance_variable_get(:@registered_namespaces).dup unless superclass.instance_variable_get(:@registered_namespaces).nil?
25
+ @wrapper_anonymous_classes = superclass.instance_variable_get(:@wrapper_anonymous_classes).dup unless superclass.instance_variable_get(:@wrapper_anonymous_classes).nil?
26
+ end
27
+ end
28
+
29
+ base.extend ClassMethods
30
+ end
31
+
32
+ module ClassMethods
33
+ #
34
+ # The xml has the following attributes defined.
35
+ #
36
+ # @example
37
+ #
38
+ # "<country code='de'>Germany</country>"
39
+ #
40
+ # # definition of the 'code' attribute within the class
41
+ # attribute :code, String
42
+ #
43
+ # @param [Symbol] name the name of the accessor that is created
44
+ # @param [String,Class] type the class name of the name of the class whcih
45
+ # the object will be converted upon parsing
46
+ # @param [Hash] options additional parameters to send to the relationship
47
+ #
48
+ def attribute(name, type, options = {})
49
+ attribute = Attribute.new(name, type, options)
50
+ @attributes[name] = attribute
51
+ attr_accessor attribute.method_name.to_sym
52
+ end
53
+
54
+ #
55
+ # The elements defined through {#attribute}.
56
+ #
57
+ # @return [Array<Attribute>] a list of the attributes defined for this class;
58
+ # an empty array is returned when there have been no attributes defined.
59
+ #
60
+ def attributes
61
+ @attributes.values
62
+ end
63
+
64
+ #
65
+ # Register a namespace that is used to persist the object namespace back to
66
+ # XML.
67
+ #
68
+ # @example
69
+ #
70
+ # register_namespace 'prefix', 'http://www.unicornland.com/prefix'
71
+ #
72
+ # # the output will contain the namespace defined
73
+ #
74
+ # "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
75
+ # ...
76
+ # </outputXML>"
77
+ #
78
+ # @param [String] namespace the xml prefix
79
+ # @param [String] ns url for the xml namespace
80
+ #
81
+ def register_namespace(namespace, ns)
82
+ @registered_namespaces.merge!(namespace => ns)
83
+ end
84
+
85
+ #
86
+ # An element defined in the XML that is parsed.
87
+ #
88
+ # @example
89
+ #
90
+ # "<address location='home'>
91
+ # <city>Oldenburg</city>
92
+ # </address>"
93
+ #
94
+ # # definition of the 'city' element within the class
95
+ #
96
+ # element :city, String
97
+ #
98
+ # @param [Symbol] name the name of the accessor that is created
99
+ # @param [String,Class] type the class name of the name of the class whcih
100
+ # the object will be converted upon parsing
101
+ # @param [Hash] options additional parameters to send to the relationship
102
+ #
103
+ def element(name, type, options = {})
104
+ element = Element.new(name, type, options)
105
+ @elements[name] = element
106
+ attr_accessor element.method_name.to_sym
107
+ end
108
+
109
+ #
110
+ # The elements defined through {#element}, {#has_one}, and {#has_many}.
111
+ #
112
+ # @return [Array<Element>] a list of the elements contained defined for this
113
+ # class; an empty array is returned when there have been no elements
114
+ # defined.
115
+ #
116
+ def elements
117
+ @elements.values
118
+ end
119
+
120
+ #
121
+ # The value stored in the text node of the current element.
122
+ #
123
+ # @example
124
+ #
125
+ # "<firstName>Michael Jackson</firstName>"
126
+ #
127
+ # # definition of the 'firstName' text node within the class
128
+ #
129
+ # content :first_name, String
130
+ #
131
+ # @param [Symbol] name the name of the accessor that is created
132
+ # @param [String,Class] type the class name of the name of the class whcih
133
+ # the object will be converted upon parsing. By Default String class will be taken.
134
+ # @param [Hash] options additional parameters to send to the relationship
135
+ #
136
+ def content(name, type = String, options = {})
137
+ @content = TextNode.new(name, type, options)
138
+ attr_accessor @content.method_name.to_sym
139
+ end
140
+
141
+ #
142
+ # Sets the object to have xml content, this will assign the XML contents
143
+ # that are parsed to the attribute accessor xml_content. The object will
144
+ # respond to the method #xml_content and will return the XML data that
145
+ # it has parsed.
146
+ #
147
+ def has_xml_content
148
+ attr_accessor :xml_content
149
+ end
150
+
151
+ #
152
+ # The object has one of these elements in the XML. If there are multiple,
153
+ # the last one will be set to this value.
154
+ #
155
+ # @param [Symbol] name the name of the accessor that is created
156
+ # @param [String,Class] type the class name of the name of the class whcih
157
+ # the object will be converted upon parsing
158
+ # @param [Hash] options additional parameters to send to the relationship
159
+ #
160
+ # @see #element
161
+ #
162
+ def has_one(name, type, options = {})
163
+ element name, type, { single: true }.merge(options)
164
+ end
165
+
166
+ #
167
+ # The object has many of these elements in the XML.
168
+ #
169
+ # @param [Symbol] name the name of accessor that is created
170
+ # @param [String,Class] type the class name or the name of the class which
171
+ # the object will be converted upon parsing.
172
+ # @param [Hash] options additional parameters to send to the relationship
173
+ #
174
+ # @see #element
175
+ #
176
+ def has_many(name, type, options = {})
177
+ element name, type, { single: false }.merge(options)
178
+ end
179
+
180
+ #
181
+ # The list of registered after_parse callbacks.
182
+ #
183
+ def after_parse_callbacks
184
+ @after_parse_callbacks ||= []
185
+ end
186
+
187
+ #
188
+ # Register a new after_parse callback, given as a block.
189
+ #
190
+ # @yield [object] Yields the newly-parsed object to the block after parsing.
191
+ # Sub-objects will be already populated.
192
+ def after_parse(&block)
193
+ after_parse_callbacks.push(block)
194
+ end
195
+
196
+ #
197
+ # Specify a namespace if a node and all its children are all namespaced
198
+ # elements. This is simpler than passing the :namespace option to each
199
+ # defined element.
200
+ #
201
+ # @param [String] namespace the namespace to set as default for the class
202
+ # element.
203
+ #
204
+ def namespace(namespace = nil)
205
+ @namespace = namespace if namespace
206
+ @namespace
207
+ end
208
+
209
+ #
210
+ # @param [String] new_tag_name the name for the tag
211
+ #
212
+ def tag(new_tag_name)
213
+ @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
214
+ end
215
+
216
+ #
217
+ # The name of the tag
218
+ #
219
+ # @return [String] the name of the tag as a string, downcased
220
+ #
221
+ def tag_name
222
+ @tag_name ||= to_s.split('::')[-1].downcase
223
+ end
224
+
225
+ # There is an XML tag that needs to be known for parsing and should be generated
226
+ # during a to_xml. But it doesn't need to be a class and the contained elements should
227
+ # be made available on the parent class
228
+ #
229
+ # @param [String] name the name of the element that is just a place holder
230
+ # @param [Proc] blk the element definitions inside the place holder tag
231
+ #
232
+ def wrap(name, &blk)
233
+ # Get an anonymous HappyMapper that has 'name' as its tag and defined
234
+ # in '&blk'. Then save that to a class instance variable for later use
235
+ wrapper = AnonymousWrapperClassFactory.get(name, &blk)
236
+ @wrapper_anonymous_classes[wrapper.inspect] = wrapper
237
+
238
+ # Create getter/setter for each element and attribute defined on the anonymous HappyMapper
239
+ # onto this class. They get/set the value by passing thru to the anonymous class.
240
+ passthrus = wrapper.attributes + wrapper.elements
241
+ passthrus.each do |item|
242
+ class_eval %{
243
+ def #{item.method_name}
244
+ @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
245
+ @#{name}.#{item.method_name}
246
+ end
247
+ def #{item.method_name}=(value)
248
+ @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
249
+ @#{name}.#{item.method_name} = value
250
+ end
251
+ }
252
+ end
253
+
254
+ has_one name, wrapper
255
+ end
256
+
257
+ # The callback defined through {.with_nokogiri_config}.
258
+ #
259
+ # @return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
260
+ #
261
+ def nokogiri_config_callback
262
+ @nokogiri_config_callback
263
+ end
264
+
265
+ # Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse().
266
+ # See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
267
+ #
268
+ # @param [Proc] the proc to pass to Nokogiri to setup parse options
269
+ #
270
+ def with_nokogiri_config(&blk)
271
+ @nokogiri_config_callback = blk
272
+ end
273
+
274
+ #
275
+ # @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
276
+ # contents to convert into Object.
277
+ # @param [Hash] options additional information for parsing. :single => true
278
+ # if requesting a single object, otherwise it defaults to retuning an
279
+ # array of multiple items. :xpath information where to start the parsing
280
+ # :namespace is the namespace to use for additional information.
281
+ #
282
+ def parse(xml, options = {})
283
+ # create a local copy of the objects namespace value for this parse execution
284
+ namespace = @namespace
285
+
286
+ # If the XML specified is an Node then we have what we need.
287
+ if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
288
+ node = xml
289
+ else
290
+
291
+ # If xml is an XML document select the root node of the document
292
+ if xml.is_a?(Nokogiri::XML::Document)
293
+ node = xml.root
294
+ else
295
+
296
+ # Attempt to parse the xml value with Nokogiri XML as a document
297
+ # and select the root element
298
+ xml = Nokogiri::XML(
299
+ xml, nil, nil,
300
+ Nokogiri::XML::ParseOptions::STRICT,
301
+ &nokogiri_config_callback
302
+ )
303
+ node = xml.root
304
+ end
305
+
306
+ # if the node name is equal to the tag name then the we are parsing the
307
+ # root element and that is important to record so that we can apply
308
+ # the correct xpath on the elements of this document.
309
+
310
+ root = node.name == tag_name
311
+ end
312
+
313
+ # if any namespaces have been provied then we should capture those and then
314
+ # merge them with any namespaces found on the xml node and merge all that
315
+ # with any namespaces that have been registered on the object
316
+
317
+ namespaces = options[:namespaces] || {}
318
+ namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
319
+ namespaces = namespaces.merge(@registered_namespaces)
320
+
321
+ # if a namespace has been provided then set the current namespace to it
322
+ # or set the default namespace to the one defined under 'xmlns'
323
+ # or set the default namespace to the namespace that matches 'happymapper's
324
+
325
+ if options[:namespace]
326
+ namespace = options[:namespace]
327
+ elsif namespaces.key?('xmlns')
328
+ namespace ||= DEFAULT_NS
329
+ namespaces[DEFAULT_NS] = namespaces.delete('xmlns')
330
+ elsif namespaces.key?(DEFAULT_NS)
331
+ namespace ||= DEFAULT_NS
332
+ end
333
+
334
+ # from the options grab any nodes present and if none are present then
335
+ # perform the following to find the nodes for the given class
336
+
337
+ nodes = options.fetch(:nodes) do
338
+ # when at the root use the xpath '/' otherwise use a more gready './/'
339
+ # unless an xpath has been specified, which should overwrite default
340
+ # and finally attach the current namespace if one has been defined
341
+ #
342
+
343
+ xpath = (root ? '/' : './/')
344
+ xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
345
+ xpath += "#{namespace}:" if namespace
346
+
347
+ nodes = []
348
+
349
+ # when finding nodes, do it in this order:
350
+ # 1. specified tag if one has been provided
351
+ # 2. name of element
352
+ # 3. tag_name (derived from class name by default)
353
+
354
+ # If a tag has been provided we need to search for it.
355
+
356
+ if options.key?(:tag)
357
+ begin
358
+ nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
359
+ rescue
360
+ # This exception takes place when the namespace is often not found
361
+ # and we should continue on with the empty array of nodes.
362
+ end
363
+ else
364
+
365
+ # This is the default case when no tag value is provided.
366
+ # First we use the name of the element `items` in `has_many items`
367
+ # Second we use the tag name which is the name of the class cleaned up
368
+
369
+ [options[:name], tag_name].compact.each do |xpath_ext|
370
+ begin
371
+ nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
372
+ rescue
373
+ break
374
+ # This exception takes place when the namespace is often not found
375
+ # and we should continue with the empty array of nodes or keep looking
376
+ end
377
+ break if nodes && !nodes.empty?
378
+ end
379
+
380
+ end
381
+
382
+ nodes
383
+ end
384
+
385
+ # Nothing matching found, we can go ahead and return
386
+ return ((options[:single] || root) ? nil : []) if nodes.size == 0
387
+
388
+ # If the :limit option has been specified then we are going to slice
389
+ # our node results by that amount to allow us the ability to deal with
390
+ # a large result set of data.
391
+
392
+ limit = options[:in_groups_of] || nodes.size
393
+
394
+ # If the limit of 0 has been specified then the user obviously wants
395
+ # none of the nodes that we are serving within this batch of nodes.
396
+
397
+ return [] if limit == 0
398
+
399
+ collection = []
400
+
401
+ nodes.each_slice(limit) do |slice|
402
+ part = slice.map do |n|
403
+ # If an existing HappyMapper object is provided, update it with the
404
+ # values from the xml being parsed. Otherwise, create a new object
405
+
406
+ obj = options[:update] ? options[:update] : new
407
+
408
+ attributes.each do |attr|
409
+ value = attr.from_xml_node(n, namespace, namespaces)
410
+ value = attr.default if value.nil?
411
+ obj.send("#{attr.method_name}=", value)
412
+ end
413
+
414
+ elements.each do |elem|
415
+ obj.send("#{elem.method_name}=", elem.from_xml_node(n, namespace, namespaces))
416
+ end
417
+
418
+ if @content
419
+ obj.send("#{@content.method_name}=", @content.from_xml_node(n, namespace, namespaces))
420
+ end
421
+
422
+ # If the HappyMapper class has the method #xml_value=,
423
+ # attr_writer :xml_value, or attr_accessor :xml_value then we want to
424
+ # assign the current xml that we just parsed to the xml_value
425
+
426
+ if obj.respond_to?('xml_value=')
427
+ n.namespaces.each { |name, path| n[name] = path }
428
+ obj.xml_value = n.to_xml
429
+ end
430
+
431
+ # If the HappyMapper class has the method #xml_content=,
432
+ # attr_write :xml_content, or attr_accessor :xml_content then we want to
433
+ # assign the child xml that we just parsed to the xml_content
434
+
435
+ if obj.respond_to?('xml_content=')
436
+ n = n.children if n.respond_to?(:children)
437
+ obj.xml_content = n.to_xml
438
+ end
439
+
440
+ # Call any registered after_parse callbacks for the object's class
441
+
442
+ obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
443
+
444
+ # collect the object that we have created
445
+
446
+ obj
447
+ end
448
+
449
+ # If a block has been provided and the user has requested that the objects
450
+ # be handled in groups then we should yield the slice of the objects to them
451
+ # otherwise continue to lump them together
452
+
453
+ if block_given? && options[:in_groups_of]
454
+ yield part
455
+ else
456
+ collection += part
457
+ end
458
+ end
459
+
460
+ # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
461
+ nodes = nil
462
+
463
+ # If the :single option has been specified or we are at the root element
464
+ # then we are going to return the first item in the collection. Otherwise
465
+ # the return response is going to be an entire array of items.
466
+
467
+ if options[:single] || root
468
+ collection.first
469
+ else
470
+ collection
471
+ end
472
+ end
473
+ end
474
+
475
+ # Set all attributes with a default to their default values
476
+ def initialize
477
+ super
478
+ self.class.attributes.reject { |attr| attr.default.nil? }.each do |attr|
479
+ send("#{attr.method_name}=", attr.default)
480
+ end
481
+ end
482
+
483
+ #
484
+ # Create an xml representation of the specified class based on defined
485
+ # HappyMapper elements and attributes. The method is defined in a way
486
+ # that it can be called recursively by classes that are also HappyMapper
487
+ # classes, allowg for the composition of classes.
488
+ #
489
+ # @param [Nokogiri::XML::Builder] builder an instance of the XML builder which
490
+ # is being used when called recursively.
491
+ # @param [String] default_namespace The name of the namespace which is the
492
+ # default for the xml being produced; this is the namespace of the
493
+ # parent
494
+ # @param [String] namespace_override The namespace specified with the element
495
+ # declaration in the parent. Overrides the namespace declaration in the
496
+ # element class itself when calling #to_xml recursively.
497
+ # @param [String] tag_from_parent The xml tag to use on the element when being
498
+ # called recursively. This lets the parent doc define its own structure.
499
+ # Otherwise the element uses the tag it has defined for itself. Should only
500
+ # apply when calling a child HappyMapper element.
501
+ #
502
+ # @return [String,Nokogiri::XML::Builder] return XML representation of the
503
+ # HappyMapper object; when called recursively this is going to return
504
+ # and Nokogiri::XML::Builder object.
505
+ #
506
+ def to_xml(builder = nil, default_namespace = nil, namespace_override = nil,
507
+ tag_from_parent = nil)
508
+
509
+ #
510
+ # If to_xml has been called without a passed in builder instance that
511
+ # means we are going to return xml output. When it has been called with
512
+ # a builder instance that means we most likely being called recursively
513
+ # and will return the end product as a builder instance.
514
+ #
515
+ unless builder
516
+ write_out_to_xml = true
517
+ builder = Nokogiri::XML::Builder.new
518
+ end
519
+
520
+ #
521
+ # Find the attributes for the class and collect them into an array
522
+ # that will be placed into a Hash structure
523
+ #
524
+ attributes = self.class.attributes.collect do |attribute|
525
+ #
526
+ # If an attribute is marked as read_only then we want to ignore the attribute
527
+ # when it comes to saving the xml document; so we wiill not go into any of
528
+ # the below process
529
+ #
530
+ unless attribute.options[:read_only]
531
+
532
+ value = send(attribute.method_name)
533
+ value = nil if value == attribute.default
534
+
535
+ #
536
+ # If the attribute defines an on_save lambda/proc or value that maps to
537
+ # a method that the class has defined, then call it with the value as a
538
+ # parameter.
539
+ #
540
+ if on_save_action = attribute.options[:on_save]
541
+ if on_save_action.is_a?(Proc)
542
+ value = on_save_action.call(value)
543
+ elsif respond_to?(on_save_action)
544
+ value = send(on_save_action, value)
545
+ end
546
+ end
547
+
548
+ #
549
+ # Attributes that have a nil value should be ignored unless they explicitly
550
+ # state that they should be expressed in the output.
551
+ #
552
+ if not value.nil? || attribute.options[:state_when_nil]
553
+ attribute_namespace = attribute.options[:namespace]
554
+ ["#{attribute_namespace ? "#{attribute_namespace}:" : ''}#{attribute.tag}", value]
555
+ else
556
+ []
557
+ end
558
+
559
+ else
560
+ []
561
+ end
562
+ end.flatten
563
+
564
+ attributes = Hash[*attributes]
565
+
566
+ #
567
+ # Create a tag in the builder that matches the class's tag name unless a tag was passed
568
+ # in a recursive call from the parent doc. Then append
569
+ # any attributes to the element that were defined above.
570
+ #
571
+ builder.send("#{tag_from_parent || self.class.tag_name}_", attributes) do |xml|
572
+ #
573
+ # Add all the registered namespaces to the root element.
574
+ # When this is called recurisvely by composed classes the namespaces
575
+ # are still added to the root element
576
+ #
577
+ # However, we do not want to add the namespace if the namespace is 'xmlns'
578
+ # which means that it is the default namesapce of the code.
579
+ #
580
+ if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
581
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |name, href|
582
+ name = nil if name == 'xmlns'
583
+ builder.doc.root.add_namespace(name, href)
584
+ end
585
+ end
586
+
587
+ #
588
+ # If the object we are serializing has a namespace declaration we will want
589
+ # to use that namespace or we will use the default namespace.
590
+ # When neither are specifed we are simply using whatever is default to the
591
+ # builder
592
+ #
593
+ namespace_for_parent = namespace_override
594
+ if self.class.respond_to?(:namespace) && self.class.namespace
595
+ namespace_for_parent ||= self.class.namespace
596
+ end
597
+ namespace_for_parent ||= default_namespace
598
+
599
+ xml.parent.namespace =
600
+ builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_for_parent }
601
+
602
+ #
603
+ # When a content has been defined we add the resulting value
604
+ # the output xml
605
+ #
606
+ if content = self.class.instance_variable_get('@content')
607
+
608
+ unless content.options[:read_only]
609
+ text_accessor = content.tag || content.name
610
+ value = send(text_accessor)
611
+
612
+ if on_save_action = content.options[:on_save]
613
+ if on_save_action.is_a?(Proc)
614
+ value = on_save_action.call(value)
615
+ elsif respond_to?(on_save_action)
616
+ value = send(on_save_action, value)
617
+ end
618
+ end
619
+
620
+ builder.text(value)
621
+ end
622
+
623
+ end
624
+
625
+ #
626
+ # for every define element (i.e. has_one, has_many, element) we are
627
+ # going to persist each one
628
+ #
629
+ self.class.elements.each do |element|
630
+ #
631
+ # If an element is marked as read only do not consider at all when
632
+ # saving to XML.
633
+ #
634
+ unless element.options[:read_only]
635
+
636
+ tag = element.tag || element.name
637
+
638
+ #
639
+ # The value to store is the result of the method call to the element,
640
+ # by default this is simply utilizing the attr_accessor defined. However,
641
+ # this allows for this method to be overridden
642
+ #
643
+ value = send(element.name)
644
+
645
+ #
646
+ # If the element defines an on_save lambda/proc then we will call that
647
+ # operation on the specified value. This allows for operations to be
648
+ # performed to convert the value to a specific value to be saved to the xml.
649
+ #
650
+ if on_save_action = element.options[:on_save]
651
+ if on_save_action.is_a?(Proc)
652
+ value = on_save_action.call(value)
653
+ elsif respond_to?(on_save_action)
654
+ value = send(on_save_action, value)
655
+ end
656
+ end
657
+
658
+ #
659
+ # Normally a nil value would be ignored, however if specified then
660
+ # an empty element will be written to the xml
661
+ #
662
+ if value.nil? && element.options[:single] && element.options[:state_when_nil]
663
+ xml.send("#{tag}_", '')
664
+ end
665
+
666
+ #
667
+ # To allow for us to treat both groups of items and singular items
668
+ # equally we wrap the value and treat it as an array.
669
+ #
670
+ if value.nil?
671
+ values = []
672
+ elsif value.respond_to?(:to_ary) && !element.options[:single]
673
+ values = value.to_ary
674
+ else
675
+ values = [value]
676
+ end
677
+
678
+ values.each do |item|
679
+ if item.is_a?(HappyMapper)
680
+
681
+ #
682
+ # Other items are convertable to xml through the xml builder
683
+ # process should have their contents retrieved and attached
684
+ # to the builder structure
685
+ #
686
+ item.to_xml(xml, self.class.namespace || default_namespace,
687
+ element.options[:namespace],
688
+ element.options[:tag] || nil)
689
+
690
+ elsif !item.nil?
691
+
692
+ item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
693
+
694
+ #
695
+ # When a value exists we should append the value for the tag
696
+ #
697
+ if item_namespace
698
+ xml[item_namespace].send("#{tag}_", item.to_s)
699
+ else
700
+ xml.send("#{tag}_", item.to_s)
701
+ end
702
+
703
+ else
704
+
705
+ #
706
+ # Normally a nil value would be ignored, however if specified then
707
+ # an empty element will be written to the xml
708
+ #
709
+ xml.send("#{tag}_", '') if element.options[:state_when_nil]
710
+
711
+ end
712
+ end
713
+
714
+ end
715
+ end
716
+ end
717
+
718
+ # Write out to XML, this value was set above, based on whether or not an XML
719
+ # builder object was passed to it as a parameter. When there was no parameter
720
+ # we assume we are at the root level of the #to_xml call and want the actual
721
+ # xml generated from the object. If an XML builder instance was specified
722
+ # then we assume that has been called recursively to generate a larger
723
+ # XML document.
724
+ write_out_to_xml ? builder.to_xml : builder
725
+ end
726
+
727
+ # Parse the xml and update this instance. This does not update instances
728
+ # of HappyMappers that are children of this object. New instances will be
729
+ # created for any HappyMapper children of this object.
730
+ #
731
+ # Params and return are the same as the class parse() method above.
732
+ def parse(xml, options = {})
733
+ self.class.parse(xml, options.merge!(update: self))
734
+ end
735
+
736
+ private
737
+
738
+ # Factory for creating anonmyous HappyMappers
739
+ class AnonymousWrapperClassFactory
740
+ def self.get(name, &blk)
741
+ Class.new do
742
+ include HappyMapper
743
+ tag name
744
+ instance_eval &blk
745
+ end
746
+ end
747
+ end
748
+ end
749
+
750
+ require 'happymapper/supported_types'
751
+ require 'happymapper/item'
752
+ require 'happymapper/attribute'
753
+ require 'happymapper/element'
754
+ require 'happymapper/text_node'