uva-happymapper 0.4.1

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