xmlmapper 0.5.9

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