nokogiri-happymapper 0.8.1 → 0.10.0

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.
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