instructure-happymapper 0.5.10

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 +35 -0
  3. data/README.md +605 -0
  4. data/lib/happymapper.rb +767 -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/spec/attribute_default_value_spec.rb +50 -0
  13. data/spec/attributes_spec.rb +36 -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 +12 -0
  42. data/spec/happymapper/element_spec.rb +9 -0
  43. data/spec/happymapper/item_spec.rb +115 -0
  44. data/spec/happymapper/text_node_spec.rb +9 -0
  45. data/spec/happymapper_parse_spec.rb +113 -0
  46. data/spec/happymapper_spec.rb +1116 -0
  47. data/spec/has_many_empty_array_spec.rb +43 -0
  48. data/spec/ignay_spec.rb +95 -0
  49. data/spec/inheritance_spec.rb +107 -0
  50. data/spec/mixed_namespaces_spec.rb +61 -0
  51. data/spec/parse_with_object_to_update_spec.rb +111 -0
  52. data/spec/spec_helper.rb +7 -0
  53. data/spec/to_xml_spec.rb +200 -0
  54. data/spec/to_xml_with_namespaces_spec.rb +196 -0
  55. data/spec/wilcard_tag_name_spec.rb +96 -0
  56. data/spec/wrap_spec.rb +82 -0
  57. data/spec/xpath_spec.rb +89 -0
  58. metadata +183 -0
@@ -0,0 +1,767 @@
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 specified by the element
505
+ # declaration when calling #to_xml recursively.
506
+ # @param [String] tag_from_parent the xml tag to use on the element when being
507
+ # called recursively. This lets the parent doc define its own structure.
508
+ # Otherwise the element uses the tag it has defined for itself. Should only
509
+ # apply when calling a child HappyMapper element.
510
+ #
511
+ # @return [String,Nokogiri::XML::Builder] return XML representation of the
512
+ # HappyMapper object; when called recursively this is going to return
513
+ # and Nokogiri::XML::Builder object.
514
+ #
515
+ def to_xml(builder = nil,default_namespace = nil,tag_from_parent = nil)
516
+
517
+ #
518
+ # If to_xml has been called without a passed in builder instance that
519
+ # means we are going to return xml output. When it has been called with
520
+ # a builder instance that means we most likely being called recursively
521
+ # and will return the end product as a builder instance.
522
+ #
523
+ unless builder
524
+ write_out_to_xml = true
525
+ builder = Nokogiri::XML::Builder.new
526
+ end
527
+
528
+ #
529
+ # Find the attributes for the class and collect them into an array
530
+ # that will be placed into a Hash structure
531
+ #
532
+ attributes = self.class.attributes.collect do |attribute|
533
+
534
+ #
535
+ # If an attribute is marked as read_only then we want to ignore the attribute
536
+ # when it comes to saving the xml document; so we wiill not go into any of
537
+ # the below process
538
+ #
539
+ unless attribute.options[:read_only]
540
+
541
+ value = send(attribute.method_name)
542
+ value = nil if value == attribute.default
543
+
544
+ #
545
+ # If the attribute defines an on_save lambda/proc or value that maps to
546
+ # a method that the class has defined, then call it with the value as a
547
+ # parameter.
548
+ #
549
+ if on_save_action = attribute.options[:on_save]
550
+ if on_save_action.is_a?(Proc)
551
+ value = on_save_action.call(value)
552
+ elsif respond_to?(on_save_action)
553
+ value = send(on_save_action,value)
554
+ end
555
+ end
556
+
557
+ #
558
+ # Attributes that have a nil value should be ignored unless they explicitly
559
+ # state that they should be expressed in the output.
560
+ #
561
+ if not value.nil? || attribute.options[:state_when_nil]
562
+ attribute_namespace = attribute.options[:namespace] || default_namespace
563
+ [ "#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value ]
564
+ else
565
+ []
566
+ end
567
+
568
+ else
569
+ []
570
+ end
571
+
572
+ end.flatten
573
+
574
+ attributes = Hash[ *attributes ]
575
+
576
+ #
577
+ # Create a tag in the builder that matches the class's tag name unless a tag was passed
578
+ # in a recursive call from the parent doc. Then append
579
+ # any attributes to the element that were defined above.
580
+ #
581
+ builder.send("#{tag_from_parent || self.class.tag_name}_",attributes) do |xml|
582
+
583
+ #
584
+ # Add all the registered namespaces to the root element.
585
+ # When this is called recurisvely by composed classes the namespaces
586
+ # are still added to the root element
587
+ #
588
+ # However, we do not want to add the namespace if the namespace is 'xmlns'
589
+ # which means that it is the default namesapce of the code.
590
+ #
591
+ if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
592
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |name,href|
593
+ name = nil if name == "xmlns"
594
+ builder.doc.root.add_namespace(name,href)
595
+ end
596
+ end
597
+
598
+ #
599
+ # If the object we are persisting has a namespace declaration we will want
600
+ # to use that namespace or we will use the default namespace.
601
+ # When neither are specifed we are simply using whatever is default to the
602
+ # builder
603
+ #
604
+ if self.class.respond_to?(:namespace) && self.class.namespace
605
+ xml.parent.namespace = builder.doc.root.namespace_definitions.find { |x| x.prefix == self.class.namespace }
606
+ elsif default_namespace
607
+ xml.parent.namespace = builder.doc.root.namespace_definitions.find { |x| x.prefix == default_namespace }
608
+ end
609
+
610
+
611
+ #
612
+ # When a content has been defined we add the resulting value
613
+ # the output xml
614
+ #
615
+ if content = self.class.instance_variable_get('@content')
616
+
617
+ unless content.options[:read_only]
618
+ text_accessor = content.tag || content.name
619
+ value = send(text_accessor)
620
+
621
+ if on_save_action = content.options[:on_save]
622
+ if on_save_action.is_a?(Proc)
623
+ value = on_save_action.call(value)
624
+ elsif respond_to?(on_save_action)
625
+ value = send(on_save_action,value)
626
+ end
627
+ end
628
+
629
+ builder.text(value)
630
+ end
631
+
632
+ end
633
+
634
+ #
635
+ # for every define element (i.e. has_one, has_many, element) we are
636
+ # going to persist each one
637
+ #
638
+ self.class.elements.each do |element|
639
+
640
+ #
641
+ # If an element is marked as read only do not consider at all when
642
+ # saving to XML.
643
+ #
644
+ unless element.options[:read_only]
645
+
646
+ tag = element.tag || element.name
647
+
648
+ #
649
+ # The value to store is the result of the method call to the element,
650
+ # by default this is simply utilizing the attr_accessor defined. However,
651
+ # this allows for this method to be overridden
652
+ #
653
+ value = send(element.name)
654
+
655
+ #
656
+ # If the element defines an on_save lambda/proc then we will call that
657
+ # operation on the specified value. This allows for operations to be
658
+ # performed to convert the value to a specific value to be saved to the xml.
659
+ #
660
+ if on_save_action = element.options[:on_save]
661
+ if on_save_action.is_a?(Proc)
662
+ value = on_save_action.call(value)
663
+ elsif respond_to?(on_save_action)
664
+ value = send(on_save_action,value)
665
+ end
666
+ end
667
+
668
+ #
669
+ # Normally a nil value would be ignored, however if specified then
670
+ # an empty element will be written to the xml
671
+ #
672
+ if value.nil? && element.options[:single] && element.options[:state_when_nil]
673
+ xml.send("#{tag}_","")
674
+ end
675
+
676
+ #
677
+ # To allow for us to treat both groups of items and singular items
678
+ # equally we wrap the value and treat it as an array.
679
+ #
680
+ if value.nil?
681
+ values = []
682
+ elsif value.respond_to?(:to_ary) && !element.options[:single]
683
+ values = value.to_ary
684
+ else
685
+ values = [value]
686
+ end
687
+
688
+ values.each do |item|
689
+
690
+ if item.is_a?(HappyMapper)
691
+
692
+ #
693
+ # Other items are convertable to xml through the xml builder
694
+ # process should have their contents retrieved and attached
695
+ # to the builder structure
696
+ #
697
+ item.to_xml(xml,element.options[:namespace],element.options[:tag] || nil)
698
+
699
+ elsif !item.nil?
700
+
701
+ item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
702
+
703
+ #
704
+ # When a value exists we should append the value for the tag
705
+ #
706
+ if item_namespace
707
+ xml[item_namespace].send("#{tag}_",item.to_s)
708
+ else
709
+ xml.send("#{tag}_",item.to_s)
710
+ end
711
+
712
+ else
713
+
714
+ #
715
+ # Normally a nil value would be ignored, however if specified then
716
+ # an empty element will be written to the xml
717
+ #
718
+ xml.send("#{tag}_","") if element.options[:state_when_nil]
719
+
720
+ end
721
+
722
+ end
723
+
724
+ end
725
+ end
726
+
727
+ end
728
+
729
+ # Write out to XML, this value was set above, based on whether or not an XML
730
+ # builder object was passed to it as a parameter. When there was no parameter
731
+ # we assume we are at the root level of the #to_xml call and want the actual
732
+ # xml generated from the object. If an XML builder instance was specified
733
+ # then we assume that has been called recursively to generate a larger
734
+ # XML document.
735
+ write_out_to_xml ? builder.to_xml : builder
736
+
737
+ end
738
+
739
+ # Parse the xml and update this instance. This does not update instances
740
+ # of HappyMappers that are children of this object. New instances will be
741
+ # created for any HappyMapper children of this object.
742
+ #
743
+ # Params and return are the same as the class parse() method above.
744
+ def parse(xml, options = {})
745
+ self.class.parse(xml, options.merge!(:update => self))
746
+ end
747
+
748
+ private
749
+
750
+ # Factory for creating anonmyous HappyMappers
751
+ class AnonymousWrapperClassFactory
752
+ def self.get(name, &blk)
753
+ Class.new do
754
+ include HappyMapper
755
+ tag name
756
+ instance_eval &blk
757
+ end
758
+ end
759
+ end
760
+
761
+ end
762
+
763
+ require 'happymapper/supported_types'
764
+ require 'happymapper/item'
765
+ require 'happymapper/attribute'
766
+ require 'happymapper/element'
767
+ require 'happymapper/text_node'