nokogiri-happymapper 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +1 -1
- data/lib/happymapper/anonymous_mapper.rb +6 -5
- data/lib/happymapper/class_methods.rb +466 -0
- data/lib/happymapper/element.rb +1 -1
- data/lib/happymapper/item.rb +19 -13
- data/lib/happymapper/supported_types.rb +2 -2
- data/lib/happymapper/syntax_error.rb +6 -0
- data/lib/happymapper/version.rb +1 -1
- data/lib/happymapper.rb +26 -469
- data/lib/nokogiri-happymapper.rb +1 -1
- metadata +16 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4037488d0324e3ef30b397bb88363d45724dbfff725c3fa34c011d6530463cce
|
4
|
+
data.tar.gz: 8db235cf492da7f19b42ff3b10b59556fd580039a31d7e0afe1041f613db67fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e74d65ab359a268fe0c0af0adba002cea497280111479f523858b063d36d6e182e1594b2cece9728f3ee10fd7d36fa024df1583e568042b915ebe502773d92d5
|
7
|
+
data.tar.gz: d5b197a8d3163ccbd27ed542546b61b6e364301ef43503c38636da40fec50f0c1a0e78f88ca161690dc2b495ea645684247c78faf9053b3ab555380750f0e91b
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,28 @@
|
|
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
|
+
|
3
26
|
## 0.9.0 / 2022-01-21
|
4
27
|
|
5
28
|
* Add official support for Ruby 2.7, 3.0 and 3.1
|
data/README.md
CHANGED
@@ -241,7 +241,7 @@ the instance variable `@streets` if we ever need to the values as an array.
|
|
241
241
|
Attributes are absolutely the same as `element` or `has_many`
|
242
242
|
|
243
243
|
```ruby
|
244
|
-
attribute :location, String, tag: 'location
|
244
|
+
attribute :location, String, tag: 'location'
|
245
245
|
```
|
246
246
|
|
247
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
|
7
|
-
#
|
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] =
|
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
|
data/lib/happymapper/element.rb
CHANGED
@@ -44,7 +44,7 @@ module HappyMapper
|
|
44
44
|
attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options)
|
45
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
|
data/lib/happymapper/item.rb
CHANGED
@@ -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(
|
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
|
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 +=
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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 =
|
@@ -104,7 +104,7 @@ module HappyMapper
|
|
104
104
|
|
105
105
|
register_type Time do |value|
|
106
106
|
Time.parse(value.to_s)
|
107
|
-
rescue
|
107
|
+
rescue ArgumentError
|
108
108
|
Time.at(value.to_i)
|
109
109
|
end
|
110
110
|
|
@@ -122,7 +122,7 @@ module HappyMapper
|
|
122
122
|
|
123
123
|
register_type Integer do |value|
|
124
124
|
value_to_i = value.to_i
|
125
|
-
if value_to_i == 0 && !value.to_s.start_with?(
|
125
|
+
if value_to_i == 0 && !value.to_s.start_with?("0")
|
126
126
|
nil
|
127
127
|
else
|
128
128
|
value_to_i
|
data/lib/happymapper/version.rb
CHANGED
data/lib/happymapper.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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
|
|
@@ -549,7 +101,9 @@ module HappyMapper
|
|
549
101
|
# in a recursive call from the parent doc. Then append
|
550
102
|
# any attributes to the element that were defined above.
|
551
103
|
#
|
552
|
-
|
104
|
+
|
105
|
+
tag_name = tag_from_parent || self.class.tag_name
|
106
|
+
builder.send(:"#{tag_name}_", attributes) do |xml|
|
553
107
|
register_namespaces_with_builder(builder)
|
554
108
|
|
555
109
|
xml.parent.namespace =
|
@@ -581,7 +135,7 @@ module HappyMapper
|
|
581
135
|
# xml generated from the object. If an XML builder instance was specified
|
582
136
|
# then we assume that has been called recursively to generate a larger
|
583
137
|
# XML document.
|
584
|
-
write_out_to_xml ? builder.to_xml.force_encoding(
|
138
|
+
write_out_to_xml ? builder.to_xml.force_encoding("UTF-8") : builder
|
585
139
|
end
|
586
140
|
|
587
141
|
# Parse the xml and update this instance. This does not update instances
|
@@ -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.
|
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,8 @@ 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}:" :
|
658
|
-
end
|
211
|
+
["#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value]
|
212
|
+
end
|
659
213
|
|
660
214
|
attributes.to_h
|
661
215
|
end
|
@@ -672,7 +226,7 @@ module HappyMapper
|
|
672
226
|
return unless self.class.instance_variable_get(:@registered_namespaces)
|
673
227
|
|
674
228
|
self.class.instance_variable_get(:@registered_namespaces).sort.each do |name, href|
|
675
|
-
name = nil if name ==
|
229
|
+
name = nil if name == "xmlns"
|
676
230
|
builder.doc.root.add_namespace(name, href)
|
677
231
|
end
|
678
232
|
end
|
@@ -723,16 +277,19 @@ module HappyMapper
|
|
723
277
|
|
724
278
|
elsif !item.nil? || element.options[:state_when_nil]
|
725
279
|
|
726
|
-
item_namespace =
|
280
|
+
item_namespace =
|
281
|
+
element.options[:namespace] ||
|
282
|
+
self.class.namespace ||
|
283
|
+
default_namespace
|
727
284
|
|
728
285
|
#
|
729
286
|
# When a value exists or the tag should always be emitted,
|
730
287
|
# we should append the value for the tag
|
731
288
|
#
|
732
289
|
if item_namespace
|
733
|
-
xml[item_namespace].send("#{tag}_", item.to_s)
|
290
|
+
xml[item_namespace].send(:"#{tag}_", item.to_s)
|
734
291
|
else
|
735
|
-
xml.send("#{tag}_", item.to_s)
|
292
|
+
xml.send(:"#{tag}_", item.to_s)
|
736
293
|
end
|
737
294
|
end
|
738
295
|
end
|
@@ -743,8 +300,8 @@ module HappyMapper
|
|
743
300
|
end
|
744
301
|
end
|
745
302
|
|
746
|
-
require
|
747
|
-
require
|
748
|
-
require
|
749
|
-
require
|
750
|
-
require
|
303
|
+
require "happymapper/supported_types"
|
304
|
+
require "happymapper/item"
|
305
|
+
require "happymapper/attribute"
|
306
|
+
require "happymapper/element"
|
307
|
+
require "happymapper/text_node"
|
data/lib/nokogiri-happymapper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nokogiri-happymapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Damien Le Berrigaud
|
@@ -14,7 +14,7 @@ authors:
|
|
14
14
|
autorequire:
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
|
-
date:
|
17
|
+
date: 2024-01-05 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: nokogiri
|
@@ -92,70 +92,70 @@ dependencies:
|
|
92
92
|
requirements:
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version: 1.
|
95
|
+
version: '1.56'
|
96
96
|
type: :development
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version: 1.
|
102
|
+
version: '1.56'
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: rubocop-packaging
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
|
-
version: 0.5.
|
109
|
+
version: 0.5.2
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
112
|
version_requirements: !ruby/object:Gem::Requirement
|
113
113
|
requirements:
|
114
114
|
- - "~>"
|
115
115
|
- !ruby/object:Gem::Version
|
116
|
-
version: 0.5.
|
116
|
+
version: 0.5.2
|
117
117
|
- !ruby/object:Gem::Dependency
|
118
118
|
name: rubocop-performance
|
119
119
|
requirement: !ruby/object:Gem::Requirement
|
120
120
|
requirements:
|
121
121
|
- - "~>"
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: 1.
|
123
|
+
version: '1.19'
|
124
124
|
type: :development
|
125
125
|
prerelease: false
|
126
126
|
version_requirements: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
128
|
- - "~>"
|
129
129
|
- !ruby/object:Gem::Version
|
130
|
-
version: 1.
|
130
|
+
version: '1.19'
|
131
131
|
- !ruby/object:Gem::Dependency
|
132
132
|
name: rubocop-rspec
|
133
133
|
requirement: !ruby/object:Gem::Requirement
|
134
134
|
requirements:
|
135
135
|
- - "~>"
|
136
136
|
- !ruby/object:Gem::Version
|
137
|
-
version: 2.
|
137
|
+
version: '2.24'
|
138
138
|
type: :development
|
139
139
|
prerelease: false
|
140
140
|
version_requirements: !ruby/object:Gem::Requirement
|
141
141
|
requirements:
|
142
142
|
- - "~>"
|
143
143
|
- !ruby/object:Gem::Version
|
144
|
-
version: 2.
|
144
|
+
version: '2.24'
|
145
145
|
- !ruby/object:Gem::Dependency
|
146
146
|
name: simplecov
|
147
147
|
requirement: !ruby/object:Gem::Requirement
|
148
148
|
requirements:
|
149
149
|
- - "~>"
|
150
150
|
- !ruby/object:Gem::Version
|
151
|
-
version: 0.
|
151
|
+
version: 0.22.0
|
152
152
|
type: :development
|
153
153
|
prerelease: false
|
154
154
|
version_requirements: !ruby/object:Gem::Requirement
|
155
155
|
requirements:
|
156
156
|
- - "~>"
|
157
157
|
- !ruby/object:Gem::Version
|
158
|
-
version: 0.
|
158
|
+
version: 0.22.0
|
159
159
|
description: Object to XML Mapping Library, using Nokogiri (fork from John Nunemaker's
|
160
160
|
Happymapper)
|
161
161
|
email: matijs@matijs.net
|
@@ -172,9 +172,11 @@ files:
|
|
172
172
|
- lib/happymapper.rb
|
173
173
|
- lib/happymapper/anonymous_mapper.rb
|
174
174
|
- lib/happymapper/attribute.rb
|
175
|
+
- lib/happymapper/class_methods.rb
|
175
176
|
- lib/happymapper/element.rb
|
176
177
|
- lib/happymapper/item.rb
|
177
178
|
- lib/happymapper/supported_types.rb
|
179
|
+
- lib/happymapper/syntax_error.rb
|
178
180
|
- lib/happymapper/text_node.rb
|
179
181
|
- lib/happymapper/version.rb
|
180
182
|
- lib/nokogiri-happymapper.rb
|
@@ -191,14 +193,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
191
193
|
requirements:
|
192
194
|
- - ">="
|
193
195
|
- !ruby/object:Gem::Version
|
194
|
-
version:
|
196
|
+
version: 3.0.0
|
195
197
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
198
|
requirements:
|
197
199
|
- - ">="
|
198
200
|
- !ruby/object:Gem::Version
|
199
201
|
version: '0'
|
200
202
|
requirements: []
|
201
|
-
rubygems_version: 3.
|
203
|
+
rubygems_version: 3.5.3
|
202
204
|
signing_key:
|
203
205
|
specification_version: 4
|
204
206
|
summary: Provides a simple way to map XML to Ruby Objects and back again.
|