blockscore-happymapper 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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'