nokogiri-happymapper 0.9.0 → 0.10.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
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
4
- require 'date'
5
- require 'time'
6
- require 'happymapper/version'
7
- require 'happymapper/anonymous_mapper'
3
+ require "nokogiri"
4
+ require "date"
5
+ require "time"
6
+ require "happymapper/version"
7
+ require "happymapper/anonymous_mapper"
8
+ require "happymapper/class_methods"
8
9
 
9
10
  module HappyMapper
10
11
  class Boolean; end
@@ -39,460 +40,11 @@ module HappyMapper
39
40
  base.extend ClassMethods
40
41
  end
41
42
 
42
- module ClassMethods
43
- #
44
- # The xml has the following attributes defined.
45
- #
46
- # @example
47
- #
48
- # "<country code='de'>Germany</country>"
49
- #
50
- # # definition of the 'code' attribute within the class
51
- # attribute :code, String
52
- #
53
- # @param [Symbol] name the name of the accessor that is created
54
- # @param [String,Class] type the class name of the name of the class whcih
55
- # the object will be converted upon parsing
56
- # @param [Hash] options additional parameters to send to the relationship
57
- #
58
- def attribute(name, type, options = {})
59
- attribute = Attribute.new(name, type, options)
60
- @attributes[name] = attribute
61
- attr_accessor attribute.method_name.intern
62
- end
63
-
64
- #
65
- # The elements defined through {#attribute}.
66
- #
67
- # @return [Array<Attribute>] a list of the attributes defined for this class;
68
- # an empty array is returned when there have been no attributes defined.
69
- #
70
- def attributes
71
- @attributes.values
72
- end
73
-
74
- #
75
- # Register a namespace that is used to persist the object namespace back to
76
- # XML.
77
- #
78
- # @example
79
- #
80
- # register_namespace 'prefix', 'http://www.unicornland.com/prefix'
81
- #
82
- # # the output will contain the namespace defined
83
- #
84
- # "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
85
- # ...
86
- # </outputXML>"
87
- #
88
- # @param [String] name the xml prefix
89
- # @param [String] href url for the xml namespace
90
- #
91
- def register_namespace(name, href)
92
- @registered_namespaces.merge!(name => href)
93
- end
94
-
95
- #
96
- # An element defined in the XML that is parsed.
97
- #
98
- # @example
99
- #
100
- # "<address location='home'>
101
- # <city>Oldenburg</city>
102
- # </address>"
103
- #
104
- # # definition of the 'city' element within the class
105
- #
106
- # element :city, String
107
- #
108
- # @param [Symbol] name the name of the accessor that is created
109
- # @param [String,Class] type the class name of the name of the class whcih
110
- # the object will be converted upon parsing
111
- # @param [Hash] options additional parameters to send to the relationship
112
- #
113
- def element(name, type, options = {})
114
- element = Element.new(name, type, options)
115
- attr_accessor element.method_name.intern unless @elements[name]
116
- @elements[name] = element
117
- end
118
-
119
- #
120
- # The elements defined through {#element}, {#has_one}, and {#has_many}.
121
- #
122
- # @return [Array<Element>] a list of the elements contained defined for this
123
- # class; an empty array is returned when there have been no elements
124
- # defined.
125
- #
126
- def elements
127
- @elements.values
128
- end
129
-
130
- #
131
- # The value stored in the text node of the current element.
132
- #
133
- # @example
134
- #
135
- # "<firstName>Michael Jackson</firstName>"
136
- #
137
- # # definition of the 'firstName' text node within the class
138
- #
139
- # content :first_name, String
140
- #
141
- # @param [Symbol] name the name of the accessor that is created
142
- # @param [String,Class] type the class name of the name of the class whcih
143
- # the object will be converted upon parsing. By Default String class will be taken.
144
- # @param [Hash] options additional parameters to send to the relationship
145
- #
146
- def content(name, type = String, options = {})
147
- @content = TextNode.new(name, type, options)
148
- attr_accessor @content.method_name.intern
149
- end
150
-
151
- #
152
- # Sets the object to have xml content, this will assign the XML contents
153
- # that are parsed to the attribute accessor xml_content. The object will
154
- # respond to the method #xml_content and will return the XML data that
155
- # it has parsed.
156
- #
157
- def has_xml_content
158
- attr_accessor :xml_content
159
- end
160
-
161
- #
162
- # The object has one of these elements in the XML. If there are multiple,
163
- # the last one will be set to this value.
164
- #
165
- # @param [Symbol] name the name of the accessor that is created
166
- # @param [String,Class] type the class name of the name of the class whcih
167
- # the object will be converted upon parsing
168
- # @param [Hash] options additional parameters to send to the relationship
169
- #
170
- # @see #element
171
- #
172
- def has_one(name, type, options = {})
173
- element name, type, { single: true }.merge(options)
174
- end
175
-
176
- #
177
- # The object has many of these elements in the XML.
178
- #
179
- # @param [Symbol] name the name of accessor that is created
180
- # @param [String,Class] type the class name or the name of the class which
181
- # the object will be converted upon parsing.
182
- # @param [Hash] options additional parameters to send to the relationship
183
- #
184
- # @see #element
185
- #
186
- def has_many(name, type, options = {})
187
- element name, type, { single: false }.merge(options)
188
- end
189
-
190
- #
191
- # The list of registered after_parse callbacks.
192
- #
193
- def after_parse_callbacks
194
- @after_parse_callbacks ||= []
195
- end
196
-
197
- #
198
- # Register a new after_parse callback, given as a block.
199
- #
200
- # @yield [object] Yields the newly-parsed object to the block after parsing.
201
- # Sub-objects will be already populated.
202
- def after_parse(&block)
203
- after_parse_callbacks.push(block)
204
- end
205
-
206
- #
207
- # Specify a namespace if a node and all its children are all namespaced
208
- # elements. This is simpler than passing the :namespace option to each
209
- # defined element.
210
- #
211
- # @param [String] namespace the namespace to set as default for the class
212
- # element.
213
- #
214
- def namespace(namespace = nil)
215
- @namespace = namespace if namespace
216
- @namespace if defined? @namespace
217
- end
218
-
219
- #
220
- # @param [String] new_tag_name the name for the tag
221
- #
222
- def tag(new_tag_name)
223
- @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
224
- end
225
-
226
- #
227
- # The name of the tag
228
- #
229
- # @return [String] the name of the tag as a string, downcased
230
- #
231
- def tag_name
232
- @tag_name ||= name && name.to_s.split('::')[-1].downcase
233
- end
234
-
235
- # There is an XML tag that needs to be known for parsing and should be generated
236
- # during a to_xml. But it doesn't need to be a class and the contained elements should
237
- # be made available on the parent class
238
- #
239
- # @param [String] name the name of the element that is just a place holder
240
- # @param [Proc] blk the element definitions inside the place holder tag
241
- #
242
- def wrap(name, &blk)
243
- # Get an anonymous HappyMapper that has 'name' as its tag and defined
244
- # in '&blk'. Then save that to a class instance variable for later use
245
- wrapper = AnonymousWrapperClassFactory.get(name, &blk)
246
- wrapper_key = wrapper.inspect
247
- @wrapper_anonymous_classes[wrapper_key] = wrapper
248
-
249
- # Create getter/setter for each element and attribute defined on the anonymous HappyMapper
250
- # onto this class. They get/set the value by passing thru to the anonymous class.
251
- passthrus = wrapper.attributes + wrapper.elements
252
- passthrus.each do |item|
253
- method_name = item.method_name
254
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
255
- def #{method_name} # def property
256
- @#{name} ||= # @wrapper ||=
257
- wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
258
- @#{name}.#{method_name} # @wrapper.property
259
- end # end
260
-
261
- def #{method_name}=(value) # def property=(value)
262
- @#{name} ||= # @wrapper ||=
263
- wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
264
- @#{name}.#{method_name} = value # @wrapper.property = value
265
- end # end
266
- RUBY
267
- end
268
-
269
- has_one name, wrapper
270
- end
271
-
272
- # The callback defined through {.with_nokogiri_config}.
273
- #
274
- # @return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
275
- #
276
- attr_reader :nokogiri_config_callback
277
-
278
- # Register a config callback according to the block Nokogori expects when
279
- # calling Nokogiri::XML::Document.parse().
280
- #
281
- # See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
282
- #
283
- # @param [Proc] the proc to pass to Nokogiri to setup parse options
284
- #
285
- def with_nokogiri_config(&blk)
286
- @nokogiri_config_callback = blk
287
- end
288
-
289
- #
290
- # @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
291
- # contents to convert into Object.
292
- # @param [Hash] options additional information for parsing.
293
- # :single => true if requesting a single object, otherwise it defaults
294
- # to retuning an array of multiple items.
295
- # :xpath information where to start the parsing
296
- # :namespace is the namespace to use for additional information.
297
- #
298
- def parse(xml, options = {})
299
- # Capture any provided namespaces and merge in any namespaces that have
300
- # been registered on the object.
301
- namespaces = options[:namespaces] || {}
302
- namespaces = namespaces.merge(@registered_namespaces)
303
-
304
- # If the XML specified is an Node then we have what we need.
305
- if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
306
- node = xml
307
- else
308
-
309
- unless xml.is_a?(Nokogiri::XML::Document)
310
- # Attempt to parse the xml value with Nokogiri XML as a document
311
- # and select the root element
312
- xml = Nokogiri::XML(
313
- xml, nil, nil,
314
- Nokogiri::XML::ParseOptions::STRICT,
315
- &nokogiri_config_callback
316
- )
317
- end
318
- # Now xml is certainly an XML document: Select the root node of the document
319
- node = xml.root
320
-
321
- # merge any namespaces found on the xml node into the namespace hash
322
- namespaces = namespaces.merge(xml.collect_namespaces)
323
-
324
- # if the node name is equal to the tag name then the we are parsing the
325
- # root element and that is important to record so that we can apply
326
- # the correct xpath on the elements of this document.
327
-
328
- root = node.name == tag_name
329
- end
330
-
331
- # If the :single option has been specified or we are at the root element
332
- # then we are going to return a single element or nil if no nodes are found
333
- single = root || options[:single]
334
-
335
- # if a namespace has been provided then set the current namespace to it
336
- # or use the namespace provided by the class
337
- # or use the 'xmlns' namespace if defined
338
-
339
- namespace = options[:namespace] || self.namespace || namespaces.key?('xmlns') && 'xmlns'
340
-
341
- # from the options grab any nodes present and if none are present then
342
- # perform the following to find the nodes for the given class
343
-
344
- nodes = options.fetch(:nodes) do
345
- find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
346
- end
347
-
348
- # Nothing matching found, we can go ahead and return
349
- return (single ? nil : []) if nodes.empty?
350
-
351
- # If the :limit option has been specified then we are going to slice
352
- # our node results by that amount to allow us the ability to deal with
353
- # a large result set of data.
354
-
355
- limit = options[:in_groups_of] || nodes.size
356
-
357
- # If the limit of 0 has been specified then the user obviously wants
358
- # none of the nodes that we are serving within this batch of nodes.
359
-
360
- return [] if limit == 0
361
-
362
- collection = []
363
-
364
- nodes.each_slice(limit) do |slice|
365
- part = slice.map do |n|
366
- parse_node(n, options, namespace, namespaces)
367
- end
368
-
369
- # If a block has been provided and the user has requested that the objects
370
- # be handled in groups then we should yield the slice of the objects to them
371
- # otherwise continue to lump them together
372
-
373
- if block_given? && options[:in_groups_of]
374
- yield part
375
- else
376
- collection += part
377
- end
378
- end
379
-
380
- # If we're parsing a single element then we are going to return the first
381
- # item in the collection. Otherwise the return response is going to be an
382
- # entire array of items.
383
-
384
- if single
385
- collection.first
386
- else
387
- collection
388
- end
389
- end
390
-
391
- # @private
392
- def defined_content
393
- @content if defined? @content
394
- end
395
-
396
- private
397
-
398
- def find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
399
- # when at the root use the xpath '/' otherwise use a more gready './/'
400
- # unless an xpath has been specified, which should overwrite default
401
- # and finally attach the current namespace if one has been defined
402
- #
403
-
404
- xpath = if options[:xpath]
405
- options[:xpath].to_s.sub(%r{([^/])$}, '\1/')
406
- elsif root
407
- '/'
408
- else
409
- './/'
410
- end
411
- if namespace
412
- return [] unless namespaces.find { |name, _url| ["xmlns:#{namespace}", namespace].include? name }
413
-
414
- xpath += "#{namespace}:"
415
- end
416
-
417
- nodes = []
418
-
419
- # when finding nodes, do it in this order:
420
- # 1. specified tag if one has been provided
421
- # 2. name of element
422
- # 3. tag_name (derived from class name by default)
423
-
424
- # If a tag has been provided we need to search for it.
425
-
426
- if options.key?(:tag)
427
- nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
428
- else
429
-
430
- # This is the default case when no tag value is provided.
431
- # First we use the name of the element `items` in `has_many items`
432
- # Second we use the tag name which is the name of the class cleaned up
433
-
434
- [options[:name], tag_name].compact.each do |xpath_ext|
435
- nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
436
- break if nodes && !nodes.empty?
437
- end
438
-
439
- end
440
-
441
- nodes
442
- end
443
-
444
- def parse_node(node, options, namespace, namespaces)
445
- # If an existing HappyMapper object is provided, update it with the
446
- # values from the xml being parsed. Otherwise, create a new object
447
-
448
- obj = options[:update] || new
449
-
450
- attributes.each do |attr|
451
- value = attr.from_xml_node(node, namespace, namespaces)
452
- value = attr.default if value.nil?
453
- obj.send("#{attr.method_name}=", value)
454
- end
455
-
456
- elements.each do |elem|
457
- obj.send("#{elem.method_name}=", elem.from_xml_node(node, namespace, namespaces))
458
- end
459
-
460
- if (content = defined_content)
461
- obj.send("#{content.method_name}=", content.from_xml_node(node, namespace, namespaces))
462
- end
463
-
464
- # If the HappyMapper class has the method #xml_value=,
465
- # attr_writer :xml_value, or attr_accessor :xml_value then we want to
466
- # assign the current xml that we just parsed to the xml_value
467
-
468
- if obj.respond_to?(:xml_value=)
469
- obj.xml_value = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
470
- end
471
-
472
- # If the HappyMapper class has the method #xml_content=,
473
- # attr_write :xml_content, or attr_accessor :xml_content then we want to
474
- # assign the child xml that we just parsed to the xml_content
475
-
476
- if obj.respond_to?(:xml_content=)
477
- node = node.children if node.respond_to?(:children)
478
- obj.xml_content = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
479
- end
480
-
481
- # Call any registered after_parse callbacks for the object's class
482
-
483
- obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
484
-
485
- # collect the object that we have created
486
-
487
- obj
488
- end
489
- end
490
-
491
43
  # Set all attributes with a default to their default values
492
44
  def initialize
493
45
  super
494
46
  self.class.attributes.reject { |attr| attr.default.nil? }.each do |attr|
495
- send("#{attr.method_name}=", attr.default)
47
+ send(:"#{attr.method_name}=", attr.default)
496
48
  end
497
49
  end
498
50
 
@@ -521,7 +73,6 @@ module HappyMapper
521
73
  #
522
74
  def to_xml(builder = nil, default_namespace = nil, namespace_override = nil,
523
75
  tag_from_parent = nil)
524
-
525
76
  #
526
77
  # If to_xml has been called without a passed in builder instance that
527
78
  # means we are going to return xml output. When it has been called with
@@ -549,7 +100,9 @@ module HappyMapper
549
100
  # in a recursive call from the parent doc. Then append
550
101
  # any attributes to the element that were defined above.
551
102
  #
552
- builder.send("#{tag_from_parent || self.class.tag_name}_", attributes) do |xml|
103
+
104
+ tag_name = tag_from_parent || self.class.tag_name
105
+ builder.send(:"#{tag_name}_", attributes) do |xml|
553
106
  register_namespaces_with_builder(builder)
554
107
 
555
108
  xml.parent.namespace =
@@ -581,7 +134,7 @@ module HappyMapper
581
134
  # xml generated from the object. If an XML builder instance was specified
582
135
  # then we assume that has been called recursively to generate a larger
583
136
  # XML document.
584
- write_out_to_xml ? builder.to_xml.force_encoding('UTF-8') : builder
137
+ write_out_to_xml ? builder.to_xml.force_encoding("UTF-8") : builder
585
138
  end
586
139
 
587
140
  # Parse the xml and update this instance. This does not update instances
@@ -598,6 +151,7 @@ module HappyMapper
598
151
  def self.get(name, &blk)
599
152
  Class.new do
600
153
  include HappyMapper
154
+
601
155
  tag name
602
156
  instance_eval(&blk)
603
157
  end
@@ -631,7 +185,7 @@ module HappyMapper
631
185
  # Find the attributes for the class and collect them into an array
632
186
  # that will be placed into a Hash structure
633
187
  #
634
- attributes = self.class.attributes.collect do |attribute|
188
+ attributes = self.class.attributes.filter_map do |attribute|
635
189
  #
636
190
  # If an attribute is marked as read_only then we want to ignore the attribute
637
191
  # when it comes to saving the xml document; so we will not go into any of
@@ -654,8 +208,13 @@ module HappyMapper
654
208
  next if value.nil? && !attribute.options[:state_when_nil]
655
209
 
656
210
  attribute_namespace = attribute.options[:namespace]
657
- ["#{attribute_namespace ? "#{attribute_namespace}:" : ''}#{attribute.tag}", value]
658
- end.compact
211
+ attribute_name = if attribute_namespace
212
+ "#{attribute_namespace}:#{attribute.tag}"
213
+ else
214
+ attribute.tag.to_s
215
+ end
216
+ [attribute_name, value]
217
+ end
659
218
 
660
219
  attributes.to_h
661
220
  end
@@ -672,7 +231,7 @@ module HappyMapper
672
231
  return unless self.class.instance_variable_get(:@registered_namespaces)
673
232
 
674
233
  self.class.instance_variable_get(:@registered_namespaces).sort.each do |name, href|
675
- name = nil if name == 'xmlns'
234
+ name = nil if name == "xmlns"
676
235
  builder.doc.root.add_namespace(name, href)
677
236
  end
678
237
  end
@@ -723,16 +282,19 @@ module HappyMapper
723
282
 
724
283
  elsif !item.nil? || element.options[:state_when_nil]
725
284
 
726
- item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
285
+ item_namespace =
286
+ element.options[:namespace] ||
287
+ self.class.namespace ||
288
+ default_namespace
727
289
 
728
290
  #
729
291
  # When a value exists or the tag should always be emitted,
730
292
  # we should append the value for the tag
731
293
  #
732
294
  if item_namespace
733
- xml[item_namespace].send("#{tag}_", item.to_s)
295
+ xml[item_namespace].send(:"#{tag}_", item.to_s)
734
296
  else
735
- xml.send("#{tag}_", item.to_s)
297
+ xml.send(:"#{tag}_", item.to_s)
736
298
  end
737
299
  end
738
300
  end
@@ -743,8 +305,8 @@ module HappyMapper
743
305
  end
744
306
  end
745
307
 
746
- require 'happymapper/supported_types'
747
- require 'happymapper/item'
748
- require 'happymapper/attribute'
749
- require 'happymapper/element'
750
- require 'happymapper/text_node'
308
+ require "happymapper/supported_types"
309
+ require "happymapper/item"
310
+ require "happymapper/attribute"
311
+ require "happymapper/element"
312
+ require "happymapper/text_node"
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # TODO: Deprecate requiring 'happymapper' in favor of 'nokogiri-happymapper'.
4
- require 'happymapper'
4
+ require "happymapper"