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