uva-happymapper 0.4.1

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