nokogiri-happymapper 0.3.6 → 0.5.1

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