nokogiri-happymapper 0.8.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +1 -4
  4. data/lib/happymapper/anonymous_mapper.rb +6 -5
  5. data/lib/happymapper/class_methods.rb +466 -0
  6. data/lib/happymapper/element.rb +3 -3
  7. data/lib/happymapper/item.rb +19 -13
  8. data/lib/happymapper/supported_types.rb +4 -6
  9. data/lib/happymapper/syntax_error.rb +6 -0
  10. data/lib/happymapper/version.rb +1 -1
  11. data/lib/happymapper.rb +54 -505
  12. data/lib/nokogiri-happymapper.rb +1 -1
  13. metadata +100 -107
  14. data/spec/features/after_parse_callbacks_spec.rb +0 -32
  15. data/spec/features/attribute_default_value_spec.rb +0 -48
  16. data/spec/features/attributes_spec.rb +0 -35
  17. data/spec/features/has_many_empty_array_spec.rb +0 -44
  18. data/spec/features/ignay_spec.rb +0 -92
  19. data/spec/features/inheritance_spec.rb +0 -121
  20. data/spec/features/mixed_namespaces_spec.rb +0 -60
  21. data/spec/features/parse_with_object_to_update_spec.rb +0 -116
  22. data/spec/features/same_tag_different_meaning_spec.rb +0 -44
  23. data/spec/features/to_xml_spec.rb +0 -205
  24. data/spec/features/to_xml_with_namespaces_spec.rb +0 -237
  25. data/spec/features/wildcard_tag_name_spec.rb +0 -110
  26. data/spec/features/wrap_spec.rb +0 -87
  27. data/spec/features/xpath_spec.rb +0 -84
  28. data/spec/fixtures/address.xml +0 -9
  29. data/spec/fixtures/ambigous_items.xml +0 -22
  30. data/spec/fixtures/analytics.xml +0 -61
  31. data/spec/fixtures/analytics_profile.xml +0 -127
  32. data/spec/fixtures/atom.xml +0 -19
  33. data/spec/fixtures/commit.xml +0 -52
  34. data/spec/fixtures/current_weather.xml +0 -89
  35. data/spec/fixtures/current_weather_missing_elements.xml +0 -18
  36. data/spec/fixtures/default_namespace_combi.xml +0 -6
  37. data/spec/fixtures/dictionary.xml +0 -20
  38. data/spec/fixtures/family_tree.xml +0 -21
  39. data/spec/fixtures/inagy.xml +0 -85
  40. data/spec/fixtures/lastfm.xml +0 -355
  41. data/spec/fixtures/multiple_namespaces.xml +0 -170
  42. data/spec/fixtures/multiple_primitives.xml +0 -5
  43. data/spec/fixtures/optional_attributes.xml +0 -6
  44. data/spec/fixtures/pita.xml +0 -133
  45. data/spec/fixtures/posts.xml +0 -23
  46. data/spec/fixtures/product_default_namespace.xml +0 -18
  47. data/spec/fixtures/product_no_namespace.xml +0 -10
  48. data/spec/fixtures/product_single_namespace.xml +0 -10
  49. data/spec/fixtures/quarters.xml +0 -19
  50. data/spec/fixtures/radar.xml +0 -21
  51. data/spec/fixtures/set_config_options.xml +0 -3
  52. data/spec/fixtures/statuses.xml +0 -422
  53. data/spec/fixtures/subclass_namespace.xml +0 -50
  54. data/spec/fixtures/unformatted_address.xml +0 -1
  55. data/spec/fixtures/wrapper.xml +0 -11
  56. data/spec/happymapper/anonymous_mapper_spec.rb +0 -158
  57. data/spec/happymapper/attribute_spec.rb +0 -12
  58. data/spec/happymapper/item_spec.rb +0 -177
  59. data/spec/happymapper_spec.rb +0 -1208
  60. data/spec/spec_helper.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29c4973fa94bd305d925426e58d62690a2929c57af50a2e9d1666957b030db0c
4
- data.tar.gz: e93f66003fc2991d3a28acf775d4224e2d2184751d082e48393c371580949803
3
+ metadata.gz: 4037488d0324e3ef30b397bb88363d45724dbfff725c3fa34c011d6530463cce
4
+ data.tar.gz: 8db235cf492da7f19b42ff3b10b59556fd580039a31d7e0afe1041f613db67fa
5
5
  SHA512:
6
- metadata.gz: 9ecf514caadcbce076cafe31127cc3a108a004b7abc75e3bfe2aa18c3d22f1fc86294c5912e43cb30f6d89b26021199d6a8e1cb3f9f63d331ba8eb4afcb2ee2f
7
- data.tar.gz: 3811e5811bc2205708e038629155a195cb949c41f11d096c13db991251d939c1e2327c8c1f80b6e5aab4783ca71f898f06ab3adaa5c20e66e57b90adce634880
6
+ metadata.gz: e74d65ab359a268fe0c0af0adba002cea497280111479f523858b063d36d6e182e1594b2cece9728f3ee10fd7d36fa024df1583e568042b915ebe502773d92d5
7
+ data.tar.gz: d5b197a8d3163ccbd27ed542546b61b6e364301ef43503c38636da40fec50f0c1a0e78f88ca161690dc2b495ea645684247c78faf9053b3ab555380750f0e91b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 / 2024-01-05
4
+
5
+ * Fix typo in README code sample ([#198] by [Spone])
6
+ * Improve custom parser option ([#219] by [dmke])
7
+ * Force namespace to be specified separately from tag ([#222] by [mvz])
8
+ * Pass options into wrapping element ([#225] by [jbennett])
9
+ * Support Ruby 3.0 through 3.3 and JRuby 9.4, dropping support for Ruby 2.6 and 2.7
10
+ ([#209], [#210], [#211] and [#230] by [mvz])
11
+
12
+ [Spone]: https://github.com/Spone
13
+ [dmke]: https://github.com/dmke
14
+ [jbennett]: https://github.com/jbennett
15
+ [mvz]: https://github.com/mvz
16
+
17
+ [#198]: https://github.com/mvz/happymapper/pull/198
18
+ [#209]: https://github.com/mvz/happymapper/pull/209
19
+ [#210]: https://github.com/mvz/happymapper/pull/210
20
+ [#211]: https://github.com/mvz/happymapper/pull/211
21
+ [#219]: https://github.com/mvz/happymapper/pull/219
22
+ [#222]: https://github.com/mvz/happymapper/pull/222
23
+ [#225]: https://github.com/mvz/happymapper/pull/225
24
+ [#230]: https://github.com/mvz/happymapper/pull/230
25
+
26
+ ## 0.9.0 / 2022-01-21
27
+
28
+ * Add official support for Ruby 2.7, 3.0 and 3.1
29
+ * Drop support for Ruby 2.3, 2.4 and 2.5
30
+ * Fix handling of attributes and elements with `state_when_nil` set
31
+ ([#165](https://github.com/mvz/happymapper/pull/165))
32
+
3
33
  ## 0.8.1 / 2019-03-18
4
34
 
5
35
  * Allow requiring `nokogiri-happymapper`
data/README.md CHANGED
@@ -7,9 +7,6 @@ This project is a fork of the great work done first by
7
7
  [jnunemaker](https://github.com/jnunemaker/happymapper).
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/nokogiri-happymapper.svg)](https://badge.fury.io/rb/nokogiri-happymapper)
10
- [![Build Status](https://travis-ci.org/mvz/happymapper.svg?branch=master)](https://travis-ci.org/mvz/happymapper)
11
- [![Coverage Status](https://coveralls.io/repos/github/mvz/happymapper/badge.svg?branch=master)](https://coveralls.io/github/mvz/happymapper?branch=master)
12
- [![Depfu](https://badges.depfu.com/badges/1707c8c2322e2ed267cf88bd4fb12b66/overview.svg)](https://depfu.com/github/mvz/happymapper)
13
10
  [![Maintainability](https://api.codeclimate.com/v1/badges/491015f82bd2a45fd9d3/maintainability)](https://codeclimate.com/github/mvz/happymapper/maintainability)
14
11
 
15
12
  ## Major Differences
@@ -244,7 +241,7 @@ the instance variable `@streets` if we ever need to the values as an array.
244
241
  Attributes are absolutely the same as `element` or `has_many`
245
242
 
246
243
  ```ruby
247
- attribute :location, String, tag: 'location
244
+ attribute :location, String, tag: 'location'
248
245
  ```
249
246
 
250
247
  Again, you can omit the tag if the attribute accessor symbol matches the name
@@ -3,8 +3,9 @@
3
3
  module HappyMapper
4
4
  class AnonymousMapper
5
5
  def parse(xml_content)
6
- # TODO: this should be able to handle all the types of functionality that parse is able
7
- # to handle which includes the text, xml document, node, fragment, etc.
6
+ # TODO: this should be able to handle all the types of functionality that
7
+ # parse is able to handle which includes the text, xml document, node,
8
+ # fragment, etc.
8
9
  xml = Nokogiri::XML(xml_content)
9
10
 
10
11
  klass = create_happymapper_class_from_node(xml.root)
@@ -26,7 +27,7 @@ module HappyMapper
26
27
  word = camel_cased_word.to_s.dup
27
28
  word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
28
29
  word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
29
- word.tr!('-', '_')
30
+ word.tr!("-", "_")
30
31
  word.downcase!
31
32
  word
32
33
  end
@@ -80,7 +81,7 @@ module HappyMapper
80
81
  # some content.
81
82
 
82
83
  if node.text?
83
- klass.content :content, String if node.content.strip != ''
84
+ klass.content :content, String if node.content.strip != ""
84
85
  return
85
86
  end
86
87
 
@@ -101,7 +102,7 @@ module HappyMapper
101
102
  options[:tag] = node.name
102
103
  namespace = node.namespace
103
104
  options[:namespace] = namespace.prefix if namespace
104
- options[:xpath] = './' unless element_type == String
105
+ options[:xpath] = "./" unless element_type == String
105
106
 
106
107
  klass.send(method, element_name, element_type, options)
107
108
  end
@@ -0,0 +1,466 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "happymapper/syntax_error"
4
+
5
+ module HappyMapper
6
+ # Class methods to be applied to classes that include the HappyMapper module.
7
+ module ClassMethods
8
+ #
9
+ # The xml has the following attributes defined.
10
+ #
11
+ # @example
12
+ #
13
+ # "<country code='de'>Germany</country>"
14
+ #
15
+ # # definition of the 'code' attribute within the class
16
+ # attribute :code, String
17
+ #
18
+ # @param [Symbol] name the name of the accessor that is created
19
+ # @param [String,Class] type the class name of the name of the class whcih
20
+ # the object will be converted upon parsing
21
+ # @param [Hash] options additional parameters to send to the relationship
22
+ #
23
+ def attribute(name, type, options = {})
24
+ attribute = Attribute.new(name, type, options)
25
+ @attributes[name] = attribute
26
+ attr_accessor attribute.method_name.intern
27
+ end
28
+
29
+ #
30
+ # The elements defined through {#attribute}.
31
+ #
32
+ # @return [Array<Attribute>] a list of the attributes defined for this class;
33
+ # an empty array is returned when there have been no attributes defined.
34
+ #
35
+ def attributes
36
+ @attributes.values
37
+ end
38
+
39
+ #
40
+ # Register a namespace that is used to persist the object namespace back to
41
+ # XML.
42
+ #
43
+ # @example
44
+ #
45
+ # register_namespace 'prefix', 'http://www.unicornland.com/prefix'
46
+ #
47
+ # # the output will contain the namespace defined
48
+ #
49
+ # "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
50
+ # ...
51
+ # </outputXML>"
52
+ #
53
+ # @param [String] name the xml prefix
54
+ # @param [String] href url for the xml namespace
55
+ #
56
+ def register_namespace(name, href)
57
+ @registered_namespaces.merge!(name => href)
58
+ end
59
+
60
+ #
61
+ # An element defined in the XML that is parsed.
62
+ #
63
+ # @example
64
+ #
65
+ # "<address location='home'>
66
+ # <city>Oldenburg</city>
67
+ # </address>"
68
+ #
69
+ # # definition of the 'city' element within the class
70
+ #
71
+ # element :city, String
72
+ #
73
+ # @param [Symbol] name the name of the accessor that is created
74
+ # @param [String,Class] type the class name of the name of the class whcih
75
+ # the object will be converted upon parsing
76
+ # @param [Hash] options additional parameters to send to the relationship
77
+ #
78
+ def element(name, type, options = {})
79
+ element = Element.new(name, type, options)
80
+ attr_accessor element.method_name.intern unless @elements[name]
81
+ @elements[name] = element
82
+ end
83
+
84
+ #
85
+ # The elements defined through {#element}, {#has_one}, and {#has_many}.
86
+ #
87
+ # @return [Array<Element>] a list of the elements contained defined for this
88
+ # class; an empty array is returned when there have been no elements
89
+ # defined.
90
+ #
91
+ def elements
92
+ @elements.values
93
+ end
94
+
95
+ #
96
+ # The value stored in the text node of the current element.
97
+ #
98
+ # @example
99
+ #
100
+ # "<firstName>Michael Jackson</firstName>"
101
+ #
102
+ # # definition of the 'firstName' text node within the class
103
+ #
104
+ # content :first_name, String
105
+ #
106
+ # @param [Symbol] name the name of the accessor that is created
107
+ # @param [String,Class] type the class name of the name of the class whcih
108
+ # the object will be converted upon parsing. By Default String class will be taken.
109
+ # @param [Hash] options additional parameters to send to the relationship
110
+ #
111
+ def content(name, type = String, options = {})
112
+ @content = TextNode.new(name, type, options)
113
+ attr_accessor @content.method_name.intern
114
+ end
115
+
116
+ #
117
+ # Sets the object to have xml content, this will assign the XML contents
118
+ # that are parsed to the attribute accessor xml_content. The object will
119
+ # respond to the method #xml_content and will return the XML data that
120
+ # it has parsed.
121
+ #
122
+ def has_xml_content
123
+ attr_accessor :xml_content
124
+ end
125
+
126
+ #
127
+ # The object has one of these elements in the XML. If there are multiple,
128
+ # the last one will be set to this value.
129
+ #
130
+ # @param [Symbol] name the name of the accessor that is created
131
+ # @param [String,Class] type the class name of the name of the class whcih
132
+ # the object will be converted upon parsing
133
+ # @param [Hash] options additional parameters to send to the relationship
134
+ #
135
+ # @see #element
136
+ #
137
+ def has_one(name, type, options = {})
138
+ element name, type, { single: true }.merge(options)
139
+ end
140
+
141
+ #
142
+ # The object has many of these elements in the XML.
143
+ #
144
+ # @param [Symbol] name the name of accessor that is created
145
+ # @param [String,Class] type the class name or the name of the class which
146
+ # the object will be converted upon parsing.
147
+ # @param [Hash] options additional parameters to send to the relationship
148
+ #
149
+ # @see #element
150
+ #
151
+ def has_many(name, type, options = {})
152
+ element name, type, { single: false }.merge(options)
153
+ end
154
+
155
+ #
156
+ # The list of registered after_parse callbacks.
157
+ #
158
+ def after_parse_callbacks
159
+ @after_parse_callbacks ||= []
160
+ end
161
+
162
+ #
163
+ # Register a new after_parse callback, given as a block.
164
+ #
165
+ # @yield [object] Yields the newly-parsed object to the block after parsing.
166
+ # Sub-objects will be already populated.
167
+ def after_parse(&block)
168
+ after_parse_callbacks.push(block)
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 if defined? @namespace
182
+ end
183
+
184
+ #
185
+ # @param [String] new_tag_name the name for the tag
186
+ #
187
+ def tag(new_tag_name)
188
+ return if new_tag_name.nil? || (name = new_tag_name.to_s).empty?
189
+
190
+ raise SyntaxError, "Unexpected ':' in tag name #{new_tag_name}" if name.include? ":"
191
+
192
+ @tag_name = name
193
+ end
194
+
195
+ #
196
+ # The name of the tag
197
+ #
198
+ # @return [String] the name of the tag as a string, downcased
199
+ #
200
+ def tag_name
201
+ @tag_name ||= name && name.to_s.split("::")[-1].downcase
202
+ end
203
+
204
+ # There is an XML tag that needs to be known for parsing and should be generated
205
+ # during a to_xml. But it doesn't need to be a class and the contained elements should
206
+ # be made available on the parent class
207
+ #
208
+ # @param [String] name the name of the element that is just a place holder
209
+ # @param [Proc] blk the element definitions inside the place holder tag
210
+ #
211
+ def wrap(name, options = {}, &blk)
212
+ # Get an anonymous HappyMapper that has 'name' as its tag and defined
213
+ # in '&blk'. Then save that to a class instance variable for later use
214
+ wrapper = AnonymousWrapperClassFactory.get(name, &blk)
215
+ wrapper_key = wrapper.inspect
216
+ @wrapper_anonymous_classes[wrapper_key] = wrapper
217
+
218
+ # Create getter/setter for each element and attribute defined on the
219
+ # anonymous HappyMapper onto this class. They get/set the value by
220
+ # passing thru to the anonymous class.
221
+ passthrus = wrapper.attributes + wrapper.elements
222
+ passthrus.each do |item|
223
+ method_name = item.method_name
224
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
225
+ def #{method_name} # def property
226
+ @#{name} ||= # @wrapper ||=
227
+ wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
228
+ @#{name}.#{method_name} # @wrapper.property
229
+ end # end
230
+
231
+ def #{method_name}=(value) # def property=(value)
232
+ @#{name} ||= # @wrapper ||=
233
+ wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
234
+ @#{name}.#{method_name} = value # @wrapper.property = value
235
+ end # end
236
+ RUBY
237
+ end
238
+
239
+ has_one name, wrapper, options
240
+ end
241
+
242
+ # The callback defined through {.with_nokogiri_config}.
243
+ #
244
+ # @return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
245
+ #
246
+ attr_reader :nokogiri_config_callback
247
+
248
+ # Register a config callback according to the block Nokogori expects when
249
+ # calling Nokogiri::XML::Document.parse().
250
+ #
251
+ # See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
252
+ #
253
+ # @param [Proc] the proc to pass to Nokogiri to setup parse options
254
+ #
255
+ def with_nokogiri_config(&blk)
256
+ @nokogiri_config_callback = blk
257
+ end
258
+
259
+ #
260
+ # @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
261
+ # contents to convert into Object.
262
+ # @param [Hash] options additional information for parsing.
263
+ # :single => true if requesting a single object, otherwise it defaults
264
+ # to retuning an array of multiple items.
265
+ # :xpath information where to start the parsing
266
+ # :namespace is the namespace to use for additional information.
267
+ #
268
+ def parse(xml, options = {})
269
+ # Capture any provided namespaces and merge in any namespaces that have
270
+ # been registered on the object.
271
+ namespaces = options[:namespaces] || {}
272
+ namespaces = namespaces.merge(@registered_namespaces)
273
+
274
+ # If the XML specified is an Node then we have what we need.
275
+ if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
276
+ node = xml
277
+ else
278
+
279
+ unless xml.is_a?(Nokogiri::XML::Document)
280
+ # Attempt to parse the xml value with Nokogiri XML as a document
281
+ # and select the root element
282
+ xml = Nokogiri::XML(
283
+ xml, nil, nil,
284
+ Nokogiri::XML::ParseOptions::STRICT,
285
+ &nokogiri_config_callback
286
+ )
287
+ end
288
+ # Now xml is certainly an XML document: Select the root node of the document
289
+ node = xml.root
290
+
291
+ # merge any namespaces found on the xml node into the namespace hash
292
+ namespaces = namespaces.merge(xml.collect_namespaces)
293
+
294
+ # if the node name is equal to the tag name then the we are parsing the
295
+ # root element and that is important to record so that we can apply
296
+ # the correct xpath on the elements of this document.
297
+
298
+ root = node.name == tag_name
299
+ end
300
+
301
+ # If the :single option has been specified or we are at the root element
302
+ # then we are going to return a single element or nil if no nodes are found
303
+ single = root || options[:single]
304
+
305
+ # if a namespace has been provided then set the current namespace to it
306
+ # or use the namespace provided by the class
307
+ # or use the 'xmlns' namespace if defined
308
+
309
+ namespace =
310
+ options[:namespace] ||
311
+ self.namespace ||
312
+ namespaces.key?("xmlns") && "xmlns"
313
+
314
+ # from the options grab any nodes present and if none are present then
315
+ # perform the following to find the nodes for the given class
316
+
317
+ nodes = options.fetch(:nodes) do
318
+ find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
319
+ end
320
+
321
+ # Nothing matching found, we can go ahead and return
322
+ return (single ? nil : []) if nodes.empty?
323
+
324
+ # If the :limit option has been specified then we are going to slice
325
+ # our node results by that amount to allow us the ability to deal with
326
+ # a large result set of data.
327
+
328
+ limit = options[:in_groups_of] || nodes.size
329
+
330
+ # If the limit of 0 has been specified then the user obviously wants
331
+ # none of the nodes that we are serving within this batch of nodes.
332
+
333
+ return [] if limit == 0
334
+
335
+ collection = []
336
+
337
+ nodes.each_slice(limit) do |slice|
338
+ part = slice.map do |n|
339
+ parse_node(n, options, namespace, namespaces)
340
+ end
341
+
342
+ # If a block has been provided and the user has requested that the objects
343
+ # be handled in groups then we should yield the slice of the objects to them
344
+ # otherwise continue to lump them together
345
+
346
+ if block_given? && options[:in_groups_of]
347
+ yield part
348
+ else
349
+ collection += part
350
+ end
351
+ end
352
+
353
+ # If we're parsing a single element then we are going to return the first
354
+ # item in the collection. Otherwise the return response is going to be an
355
+ # entire array of items.
356
+
357
+ if single
358
+ collection.first
359
+ else
360
+ collection
361
+ end
362
+ end
363
+
364
+ # @private
365
+ def defined_content
366
+ @content if defined? @content
367
+ end
368
+
369
+ private
370
+
371
+ def find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
372
+ # when at the root use the xpath '/' otherwise use a more gready './/'
373
+ # unless an xpath has been specified, which should overwrite default
374
+ # and finally attach the current namespace if one has been defined
375
+ #
376
+
377
+ xpath = if options[:xpath]
378
+ options[:xpath].to_s.sub(%r{([^/])$}, '\1/')
379
+ elsif root
380
+ "/"
381
+ else
382
+ ".//"
383
+ end
384
+ if namespace
385
+ unless namespaces.find { |name, _| ["xmlns:#{namespace}", namespace].include? name }
386
+ return []
387
+ end
388
+
389
+ xpath += "#{namespace}:"
390
+ end
391
+
392
+ nodes = []
393
+
394
+ # when finding nodes, do it in this order:
395
+ # 1. specified tag if one has been provided
396
+ # 2. name of element
397
+ # 3. tag_name (derived from class name by default)
398
+
399
+ # If a tag has been provided we need to search for it.
400
+
401
+ if options.key?(:tag)
402
+ nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
403
+ else
404
+
405
+ # This is the default case when no tag value is provided.
406
+ # First we use the name of the element `items` in `has_many items`
407
+ # Second we use the tag name which is the name of the class cleaned up
408
+
409
+ [options[:name], tag_name].compact.each do |xpath_ext|
410
+ nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
411
+ break if nodes && !nodes.empty?
412
+ end
413
+
414
+ end
415
+
416
+ nodes
417
+ end
418
+
419
+ def parse_node(node, options, namespace, namespaces)
420
+ # If an existing HappyMapper object is provided, update it with the
421
+ # values from the xml being parsed. Otherwise, create a new object
422
+
423
+ obj = options[:update] || new
424
+
425
+ attributes.each do |attr|
426
+ value = attr.from_xml_node(node, namespace, namespaces)
427
+ value = attr.default if value.nil?
428
+ obj.send(:"#{attr.method_name}=", value)
429
+ end
430
+
431
+ elements.each do |elem|
432
+ obj.send(:"#{elem.method_name}=", elem.from_xml_node(node, namespace, namespaces))
433
+ end
434
+
435
+ if (content = defined_content)
436
+ obj.send(:"#{content.method_name}=",
437
+ content.from_xml_node(node, namespace, namespaces))
438
+ end
439
+
440
+ # If the HappyMapper class has the method #xml_value=,
441
+ # attr_writer :xml_value, or attr_accessor :xml_value then we want to
442
+ # assign the current xml that we just parsed to the xml_value
443
+
444
+ if obj.respond_to?(:xml_value=)
445
+ obj.xml_value = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
446
+ end
447
+
448
+ # If the HappyMapper class has the method #xml_content=,
449
+ # attr_write :xml_content, or attr_accessor :xml_content then we want to
450
+ # assign the child xml that we just parsed to the xml_content
451
+
452
+ if obj.respond_to?(:xml_content=)
453
+ node = node.children if node.respond_to?(:children)
454
+ obj.xml_content = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
455
+ end
456
+
457
+ # Call any registered after_parse callbacks for the object's class
458
+
459
+ obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
460
+
461
+ # collect the object that we have created
462
+
463
+ obj
464
+ end
465
+ end
466
+ end
@@ -41,10 +41,10 @@ module HappyMapper
41
41
  result.attribute_nodes.each do |xml_attribute|
42
42
  next unless (attribute_options = options[:attributes][xml_attribute.name.to_sym])
43
43
 
44
- attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).
45
- from_xml_node(result, namespace, xpath_options)
44
+ attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options)
45
+ .from_xml_node(result, namespace, xpath_options)
46
46
 
47
- method_name = xml_attribute.name.tr('-', '_')
47
+ method_name = xml_attribute.name.tr("-", "_")
48
48
  value.define_singleton_method(method_name) { attribute_value }
49
49
  end
50
50
  end
@@ -20,7 +20,7 @@ module HappyMapper
20
20
  self.tag = options[:tag] || name.to_s
21
21
  self.options = { single: true }.merge(options.merge(name: self.name))
22
22
 
23
- @xml_type = self.class.to_s.split('::').last.downcase
23
+ @xml_type = self.class.to_s.split("::").last.downcase
24
24
  end
25
25
 
26
26
  def constant
@@ -35,20 +35,20 @@ module HappyMapper
35
35
  def from_xml_node(node, namespace, xpath_options)
36
36
  namespace = options[:namespace] if options.key?(:namespace)
37
37
 
38
- if suported_type_registered?
38
+ if custom_parser_defined?
39
+ find(node, namespace, xpath_options) { |n| process_node_with_custom_parser(n) }
40
+ elsif suported_type_registered?
39
41
  find(node, namespace, xpath_options) { |n| process_node_as_supported_type(n) }
40
42
  elsif constant == XmlContent
41
43
  find(node, namespace, xpath_options) { |n| process_node_as_xml_content(n) }
42
- elsif custom_parser_defined?
43
- find(node, namespace, xpath_options) { |n| process_node_with_custom_parser(n) }
44
44
  else
45
45
  process_node_with_default_parser(node, namespaces: xpath_options)
46
46
  end
47
47
  end
48
48
 
49
49
  def xpath(namespace = self.namespace)
50
- xpath = ''
51
- xpath += './/' if options[:deep]
50
+ xpath = ""
51
+ xpath += ".//" if options[:deep]
52
52
  xpath += "#{namespace}:" if namespace
53
53
  xpath += tag
54
54
  # puts "xpath: #{xpath}"
@@ -56,7 +56,7 @@ module HappyMapper
56
56
  end
57
57
 
58
58
  def method_name
59
- @method_name ||= name.tr('-', '_')
59
+ @method_name ||= name.tr("-", "_")
60
60
  end
61
61
 
62
62
  #
@@ -118,11 +118,17 @@ module HappyMapper
118
118
  node.to_s
119
119
  end
120
120
 
121
- begin
122
- constant.send(options[:parser].to_sym, value)
123
- rescue StandardError
124
- nil
125
- end
121
+ custom_parser = create_custom_parser(options[:parser])
122
+
123
+ custom_parser.call(value)
124
+ end
125
+
126
+ def create_custom_parser(parser)
127
+ return parser if parser.respond_to?(:call)
128
+
129
+ proc { |value|
130
+ constant.send(parser.to_sym, value)
131
+ }
126
132
  end
127
133
 
128
134
  def process_node_with_default_parser(node, parse_options)
@@ -142,7 +148,7 @@ module HappyMapper
142
148
  end
143
149
 
144
150
  def convert_string_to_constant(type)
145
- names = type.split('::')
151
+ names = type.split("::")
146
152
  constant = Object
147
153
  names.each do |name|
148
154
  constant =
@@ -103,11 +103,9 @@ module HappyMapper
103
103
  register_type Float, &:to_f
104
104
 
105
105
  register_type Time do |value|
106
- begin
107
- Time.parse(value.to_s)
108
- rescue StandardError
109
- Time.at(value.to_i)
110
- end
106
+ Time.parse(value.to_s)
107
+ rescue ArgumentError
108
+ Time.at(value.to_i)
111
109
  end
112
110
 
113
111
  register_type DateTime do |value|
@@ -124,7 +122,7 @@ module HappyMapper
124
122
 
125
123
  register_type Integer do |value|
126
124
  value_to_i = value.to_i
127
- if value_to_i == 0 && !value.to_s.start_with?('0')
125
+ if value_to_i == 0 && !value.to_s.start_with?("0")
128
126
  nil
129
127
  else
130
128
  value_to_i
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HappyMapper
4
+ class SyntaxError < ::StandardError
5
+ end
6
+ end