xml_data_extractor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +53 -0
- data/LICENSE.txt +21 -0
- data/README.md +381 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/src/extract/array_of.rb +58 -0
- data/lib/src/extract/array_value.rb +10 -0
- data/lib/src/extract/base.rb +12 -0
- data/lib/src/extract/expression.rb +20 -0
- data/lib/src/extract/hash_builder.rb +33 -0
- data/lib/src/extract/string_value.rb +32 -0
- data/lib/src/extract/value_builder.rb +44 -0
- data/lib/src/extract/within.rb +11 -0
- data/lib/src/extractor.rb +236 -0
- data/lib/src/format/formatter.rb +28 -0
- data/lib/src/format/mapper.rb +28 -0
- data/lib/src/format/modifier.rb +37 -0
- data/lib/src/node.rb +26 -0
- data/lib/xml_data_extractor.rb +27 -0
- data/xml_data_extractor.gemspec +28 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2ea176d26d7d1e43ca91ab838b75eca58044da1af8a98675dc3c1c1ba453f313
|
4
|
+
data.tar.gz: b4a69acdf185a5b830ef267cbb4cd86a08562795f178d5c0f79c4b893501a3b7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9bd4f5a9ea20e5ce63d92e9d213267eb06097f4a43e851bd65f622404a5f180e877409164ba5716d9e49dfa29ef1bb9b4cf8ff17ae269867ea74d2b040a7b85d
|
7
|
+
data.tar.gz: 21e37f2de2a5bc185fc2f6a30ea0367e67f9130f44a2645d53e10e1e7015e7569e47ccc7bb3e98203a12bae853bcff3d5d8ad60ff5ced36b702934a74a54bef2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
xml_data_extractor (0.1.0)
|
5
|
+
activesupport
|
6
|
+
nokogiri
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (6.0.3.2)
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
i18n (>= 0.7, < 2)
|
14
|
+
minitest (~> 5.1)
|
15
|
+
tzinfo (~> 1.1)
|
16
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
17
|
+
concurrent-ruby (1.1.7)
|
18
|
+
diff-lcs (1.3)
|
19
|
+
i18n (1.8.5)
|
20
|
+
concurrent-ruby (~> 1.0)
|
21
|
+
mini_portile2 (2.4.0)
|
22
|
+
minitest (5.14.1)
|
23
|
+
nokogiri (1.10.10)
|
24
|
+
mini_portile2 (~> 2.4.0)
|
25
|
+
rake (12.3.3)
|
26
|
+
rspec (3.9.0)
|
27
|
+
rspec-core (~> 3.9.0)
|
28
|
+
rspec-expectations (~> 3.9.0)
|
29
|
+
rspec-mocks (~> 3.9.0)
|
30
|
+
rspec-core (3.9.2)
|
31
|
+
rspec-support (~> 3.9.3)
|
32
|
+
rspec-expectations (3.9.2)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.9.0)
|
35
|
+
rspec-mocks (3.9.1)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.9.0)
|
38
|
+
rspec-support (3.9.3)
|
39
|
+
thread_safe (0.3.6)
|
40
|
+
tzinfo (1.2.7)
|
41
|
+
thread_safe (~> 0.1)
|
42
|
+
zeitwerk (2.4.0)
|
43
|
+
|
44
|
+
PLATFORMS
|
45
|
+
ruby
|
46
|
+
|
47
|
+
DEPENDENCIES
|
48
|
+
rake (~> 12.0)
|
49
|
+
rspec (~> 3.0)
|
50
|
+
xml_data_extractor!
|
51
|
+
|
52
|
+
BUNDLED WITH
|
53
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Fernando Almeida
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
# XmlDataExtractor
|
2
|
+
|
3
|
+
This gem provides a DSL for extracting formatted data from any XML structure.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'xml_data_extractor'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install xml_data_extractor
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
The general ideia is to declare a ruby Hash that represents the fields structure, containing instructions of how every piece of data should be retrieved from the XML document.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
structure = { schemas: { character: { path: "xml/FirstName" } } }
|
27
|
+
xml = "<xml><FirstName>Gandalf</FirstName></xml>"
|
28
|
+
|
29
|
+
result = XmlDataExtractor.new(structure).parse(xml)
|
30
|
+
|
31
|
+
# result -> { character: "Gandalf" }
|
32
|
+
```
|
33
|
+
|
34
|
+
For convenience, you can write the structure in yaml, which can be easily converted to a ruby hash using `YAML.load(yml).deep_symbolize_keys`.
|
35
|
+
|
36
|
+
Considering the following yaml and xml:
|
37
|
+
|
38
|
+
```yml
|
39
|
+
schemas:
|
40
|
+
description:
|
41
|
+
path: xml/desc
|
42
|
+
modifier: downcase
|
43
|
+
amount:
|
44
|
+
path: xml/info/price
|
45
|
+
modifier: to_f
|
46
|
+
```
|
47
|
+
```xml
|
48
|
+
<xml>
|
49
|
+
<desc>HELLO WORLD</desc>
|
50
|
+
<info>
|
51
|
+
<price>123</price>
|
52
|
+
</info>
|
53
|
+
</xml>
|
54
|
+
```
|
55
|
+
|
56
|
+
The output is:
|
57
|
+
```ruby
|
58
|
+
{
|
59
|
+
description: "hello world",
|
60
|
+
amount: 123.0
|
61
|
+
}
|
62
|
+
```
|
63
|
+
|
64
|
+
### Defining the structure
|
65
|
+
|
66
|
+
The structure should be defined as a hash inside the `schemas` key. See the [complete example](https://github.com/monde-sistemas/xml_data_extractor/blob/master/spec/complete_example_spec.rb#L5).
|
67
|
+
|
68
|
+
When defining the structure you can combine any available command in order to extract and format the data as needed.
|
69
|
+
|
70
|
+
The available commands are separated in two general pusposes:
|
71
|
+
|
72
|
+
- [Navigation & Extraction](#navigation--extraction)
|
73
|
+
- [Formatting](#formatting)
|
74
|
+
|
75
|
+
### Navigation & Extraction:
|
76
|
+
|
77
|
+
The data extraction process is based on `Xpath` using Nokogiri.
|
78
|
+
* [Xpath introduction](https://blog.scrapinghub.com/2016/10/27/an-introduction-to-xpath-with-examples)
|
79
|
+
* [Xpath cheatsheet](https://devhints.io/xpath)
|
80
|
+
|
81
|
+
#### path
|
82
|
+
|
83
|
+
Defines the `xpath` of the element.
|
84
|
+
The `path` is the default command of a field definition, so this:
|
85
|
+
```yml
|
86
|
+
schemas:
|
87
|
+
description:
|
88
|
+
path: xml/desc
|
89
|
+
```
|
90
|
+
Is equivalent to this:
|
91
|
+
```yml
|
92
|
+
schemas:
|
93
|
+
description: xml/desc
|
94
|
+
```
|
95
|
+
|
96
|
+
It can be defined as a string:
|
97
|
+
```yml
|
98
|
+
schemas:
|
99
|
+
description:
|
100
|
+
path: xml/some_field
|
101
|
+
```
|
102
|
+
```xml
|
103
|
+
<xml>
|
104
|
+
<some_field>ABC</some_field>
|
105
|
+
</xml>
|
106
|
+
```
|
107
|
+
```ruby
|
108
|
+
{ description: "ABC" }
|
109
|
+
```
|
110
|
+
|
111
|
+
Or as a string array:
|
112
|
+
```yml
|
113
|
+
schemas:
|
114
|
+
address:
|
115
|
+
path: [street, info/city]
|
116
|
+
```
|
117
|
+
```xml
|
118
|
+
<xml>
|
119
|
+
<street>Diagon Alley</street>
|
120
|
+
<info>
|
121
|
+
<city>London</city>
|
122
|
+
</info>
|
123
|
+
</xml>
|
124
|
+
```
|
125
|
+
```ruby
|
126
|
+
{ address: ["Diagon Alley", "London"] }
|
127
|
+
```
|
128
|
+
|
129
|
+
And even as a hash array, for complex operations:
|
130
|
+
```yml
|
131
|
+
schemas:
|
132
|
+
address:
|
133
|
+
path:
|
134
|
+
- path: street
|
135
|
+
modifier: downcase
|
136
|
+
- path: info/city
|
137
|
+
modifier: upcase
|
138
|
+
```
|
139
|
+
```ruby
|
140
|
+
{ address: ["diagon alley", "LONDON"] }
|
141
|
+
```
|
142
|
+
|
143
|
+
#### attr
|
144
|
+
|
145
|
+
Defines a tag attribute which the value should be extracted from, instead of the tag value itself:
|
146
|
+
```yml
|
147
|
+
schemas:
|
148
|
+
description:
|
149
|
+
path: xml/info
|
150
|
+
attr: desc
|
151
|
+
```
|
152
|
+
```xml
|
153
|
+
<xml>
|
154
|
+
<info desc="ABC">some stuff<info>
|
155
|
+
</xml>
|
156
|
+
```
|
157
|
+
```ruby
|
158
|
+
{ description: "ABC" }
|
159
|
+
```
|
160
|
+
|
161
|
+
Like the path, it can also be defined as a string array.
|
162
|
+
|
163
|
+
#### within
|
164
|
+
|
165
|
+
To define a root path for the fields:
|
166
|
+
```yml
|
167
|
+
schemas:
|
168
|
+
movie:
|
169
|
+
within: info/movie_data
|
170
|
+
title: original_title
|
171
|
+
actor: main_actor
|
172
|
+
|
173
|
+
```
|
174
|
+
```xml
|
175
|
+
<xml>
|
176
|
+
<info>
|
177
|
+
<movie_data>
|
178
|
+
<original_title>The Irishman</original_title>
|
179
|
+
<main_actor>Robert De Niro</main_actor>
|
180
|
+
</movie_data>
|
181
|
+
</info>
|
182
|
+
</xml>
|
183
|
+
```
|
184
|
+
```ruby
|
185
|
+
{ movie: { title: "The Irishman", actor: "Robert De Niro" } }
|
186
|
+
```
|
187
|
+
|
188
|
+
#### array_of
|
189
|
+
|
190
|
+
Defines the path to a XML collection, which will be looped generating an array of hashes:
|
191
|
+
```yml
|
192
|
+
schemas:
|
193
|
+
people:
|
194
|
+
array_of: characters/character
|
195
|
+
name: firstname
|
196
|
+
age: age
|
197
|
+
```
|
198
|
+
```xml
|
199
|
+
<xml>
|
200
|
+
<characters>
|
201
|
+
<character>
|
202
|
+
<firstname>Geralt</firstname>
|
203
|
+
<age>97</age>
|
204
|
+
</character>
|
205
|
+
<character>
|
206
|
+
<firstname>Yennefer</firstname>
|
207
|
+
<age>102</age>
|
208
|
+
</character>
|
209
|
+
</characters>
|
210
|
+
</xml>
|
211
|
+
```
|
212
|
+
```ruby
|
213
|
+
{
|
214
|
+
people: [
|
215
|
+
{ name: "Geralt", age: "97" },
|
216
|
+
{ name: "Yennefer", age: "102" }
|
217
|
+
]
|
218
|
+
}
|
219
|
+
```
|
220
|
+
|
221
|
+
If you need to loop trough nested collections, you can define an array of paths:
|
222
|
+
```yml
|
223
|
+
schemas:
|
224
|
+
show:
|
225
|
+
within: show_data
|
226
|
+
title: description
|
227
|
+
people:
|
228
|
+
array_of: [characters/character, info]
|
229
|
+
name: name
|
230
|
+
```
|
231
|
+
```xml
|
232
|
+
<xml>
|
233
|
+
<show_data>
|
234
|
+
<description>Peaky Blinders</description>
|
235
|
+
<characters>
|
236
|
+
<character>
|
237
|
+
<info>
|
238
|
+
<name>Tommy Shelby</name>
|
239
|
+
</info>
|
240
|
+
</character>
|
241
|
+
<character>
|
242
|
+
<info>
|
243
|
+
<name>Arthur Shelby</name>
|
244
|
+
</info>
|
245
|
+
<info>
|
246
|
+
<name>Alfie Solomons</name>
|
247
|
+
</info>
|
248
|
+
</character>
|
249
|
+
</characters>
|
250
|
+
</show_data>
|
251
|
+
</xml>
|
252
|
+
```
|
253
|
+
```ruby
|
254
|
+
{
|
255
|
+
show: {
|
256
|
+
title: "Peaky Blinders",
|
257
|
+
people: [
|
258
|
+
{ name: "Tommy Shelby" },
|
259
|
+
{ name: "Arthur Shelby" },
|
260
|
+
{ name: "Alfie Solomons" }
|
261
|
+
]
|
262
|
+
}
|
263
|
+
}
|
264
|
+
```
|
265
|
+
|
266
|
+
### Formatting:
|
267
|
+
|
268
|
+
#### fixed
|
269
|
+
|
270
|
+
Defines a fixed value for the field:
|
271
|
+
```yml
|
272
|
+
currency:
|
273
|
+
fixed: BRL
|
274
|
+
```
|
275
|
+
```ruby
|
276
|
+
{ currency: "BRL" }
|
277
|
+
```
|
278
|
+
|
279
|
+
#### mapper
|
280
|
+
|
281
|
+
Uses a hash of predefined values to replace the extracted value with its respective option.
|
282
|
+
If the extracted value is not found in any of the mapper options, it will be replaced by the `default` value, but if the default value is not defined, the returned value is not replaced.
|
283
|
+
```yml
|
284
|
+
mappers:
|
285
|
+
currencies:
|
286
|
+
default: unknown
|
287
|
+
options:
|
288
|
+
BRL: R$
|
289
|
+
USD: [US$, $]
|
290
|
+
schemas:
|
291
|
+
money:
|
292
|
+
array_of: curr_types/type
|
293
|
+
path: symbol
|
294
|
+
mapper: currencies
|
295
|
+
```
|
296
|
+
```xml
|
297
|
+
<xml>
|
298
|
+
<curr_type>
|
299
|
+
<type>
|
300
|
+
<symbol>US$</symbol>
|
301
|
+
</type>
|
302
|
+
<type>
|
303
|
+
<symbol>R$</symbol>
|
304
|
+
</type>
|
305
|
+
<type>
|
306
|
+
<symbol>RB</symbol>
|
307
|
+
</type>
|
308
|
+
<type>
|
309
|
+
<symbol>$</symbol>
|
310
|
+
</type>
|
311
|
+
</curr_type>
|
312
|
+
</xml>
|
313
|
+
```
|
314
|
+
```ruby
|
315
|
+
{
|
316
|
+
money: ["USD", "BRL", "unknown", "USD"]
|
317
|
+
}
|
318
|
+
```
|
319
|
+
|
320
|
+
#### modifier
|
321
|
+
|
322
|
+
Defines a method to be called on the returned value.
|
323
|
+
```yml
|
324
|
+
schemas:
|
325
|
+
name:
|
326
|
+
path: some_field
|
327
|
+
modifier: upcase
|
328
|
+
```
|
329
|
+
```xml
|
330
|
+
<xml>
|
331
|
+
<some_field>Lewandovski</some_field>
|
332
|
+
</xml>
|
333
|
+
```
|
334
|
+
```ruby
|
335
|
+
{ name: "LEWANDOVSKI" }
|
336
|
+
```
|
337
|
+
|
338
|
+
You can also pass parameters to the method. In this case you will have to declare the modifier as an array of hashes, with the `name` and `params` keys:
|
339
|
+
```yml
|
340
|
+
schemas:
|
341
|
+
name:
|
342
|
+
path: [firstname, lastname]
|
343
|
+
modifier:
|
344
|
+
- name: join
|
345
|
+
params: [" "]
|
346
|
+
- downcase
|
347
|
+
```
|
348
|
+
```xml
|
349
|
+
<xml>
|
350
|
+
<firstname>Robert</firstname>
|
351
|
+
<lastname>Martin</lastname>
|
352
|
+
</xml>
|
353
|
+
```
|
354
|
+
```ruby
|
355
|
+
{ name: "robert martin" }
|
356
|
+
```
|
357
|
+
|
358
|
+
If you need to use custom methods, you can pass an object containing the methods in the initialization. The custom method will receive the value as parameter:
|
359
|
+
```yml
|
360
|
+
schemas:
|
361
|
+
name:
|
362
|
+
path: final_price
|
363
|
+
modifier: format_as_float
|
364
|
+
```
|
365
|
+
```xml
|
366
|
+
<xml>
|
367
|
+
<final_price>R$ 12.99</final_price>
|
368
|
+
</xml>
|
369
|
+
```
|
370
|
+
```ruby
|
371
|
+
class MyMethods
|
372
|
+
def format_as_float(value)
|
373
|
+
value.gsub(/[^\d.]/, "").to_f
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
XmlDataExtractor.new(yml, MyMethods.new).parse(xml)
|
378
|
+
```
|
379
|
+
```ruby
|
380
|
+
{ price: 12.99 }
|
381
|
+
```
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "xml_data_extractor"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Extract
|
2
|
+
class ArrayOf < Base
|
3
|
+
def initialize(node, extractor, index = 0)
|
4
|
+
super(node, extractor)
|
5
|
+
@index = index
|
6
|
+
end
|
7
|
+
|
8
|
+
def value
|
9
|
+
process_paths.flatten.compact
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
attr_reader :index
|
15
|
+
|
16
|
+
def array_items
|
17
|
+
arr_path, link_path, uniq_by = node.array_of_paths
|
18
|
+
|
19
|
+
paths = extractor.paths_of(node.path, arr_path, link_path)
|
20
|
+
paths = uniq_paths(paths, uniq_by) if uniq_by
|
21
|
+
|
22
|
+
paths.each_with_index.map do |path, idx|
|
23
|
+
HashBuilder.new(Node.new(node.props, path), extractor).value(index + idx)
|
24
|
+
end.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_paths
|
28
|
+
paths = paths_from_props
|
29
|
+
|
30
|
+
if paths.size > 1
|
31
|
+
process_path(paths.shift, paths)
|
32
|
+
else
|
33
|
+
node.props[:array_of] = paths.first
|
34
|
+
array_items
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_path(path, inner_paths)
|
39
|
+
path = build_path(path) if path.is_a?(Hash)
|
40
|
+
|
41
|
+
extractor.paths_of(node.path, path).each_with_index.map do |some, idx|
|
42
|
+
ArrayOf.new(Node.new(node.props.merge(array_of: inner_paths), some), extractor, index + idx).value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def uniq_paths(paths, uniq_by)
|
47
|
+
extractor.uniq_paths(paths, uniq_by)
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_path(hash)
|
51
|
+
extractor.replace_link(hash[:path], [node.path, hash[:link]].join("/"))
|
52
|
+
end
|
53
|
+
|
54
|
+
def paths_from_props
|
55
|
+
[node.props[:array_of]].flatten
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Extract
|
2
|
+
class Expression
|
3
|
+
def initialize(expression, hash)
|
4
|
+
@expression = expression
|
5
|
+
@hash = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate
|
9
|
+
field_name = expression.split.first.parameterize
|
10
|
+
field_value = hash[field_name.to_sym]
|
11
|
+
condition = expression.gsub(field_name, field_value.to_s)
|
12
|
+
|
13
|
+
eval(condition)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :expression, :hash
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Extract
|
2
|
+
class HashBuilder < Base
|
3
|
+
INTERNAL_FIELDS = %i[array_of keep_if within].freeze
|
4
|
+
|
5
|
+
def value(index = 0)
|
6
|
+
path, props = node.to_h.values_at(:path, :props)
|
7
|
+
|
8
|
+
hash = {}
|
9
|
+
props.each do |field_name, nested_props|
|
10
|
+
next unless valuable_field? field_name, nested_props, index
|
11
|
+
|
12
|
+
value = ValueBuilder.new(Node.new(nested_props, path), extractor).value
|
13
|
+
hash[field_name.to_sym] = value if value.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
keep_hash?(hash, props) ? hash : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def keep_hash?(hash, props)
|
22
|
+
expression = props[:keep_if]
|
23
|
+
expression.present? ? Expression.new(expression, hash).evaluate : true
|
24
|
+
end
|
25
|
+
|
26
|
+
def valuable_field?(field_name, props, index)
|
27
|
+
return false if INTERNAL_FIELDS.include? field_name
|
28
|
+
return false if index.positive? && Node.new(props, "").first_only?
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Extract
|
2
|
+
class StringValue < Base
|
3
|
+
def value
|
4
|
+
path = node[:props][:path]
|
5
|
+
return formatted_array_values(path) if path.is_a?(Array)
|
6
|
+
|
7
|
+
extract_value(node)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def extract_value(node_to_extract)
|
13
|
+
extractor.extract(node_to_extract)
|
14
|
+
end
|
15
|
+
|
16
|
+
def formatted_array_values(paths)
|
17
|
+
extractor.format_value(values_from_array(paths), node[:props])
|
18
|
+
end
|
19
|
+
|
20
|
+
def values_from_array(paths)
|
21
|
+
node_path = node.path
|
22
|
+
|
23
|
+
paths.map do |inner|
|
24
|
+
if inner.is_a?(String)
|
25
|
+
extract_value(Node.new({ path: inner }, node_path))
|
26
|
+
else
|
27
|
+
StringValue.new(Node.new(inner, node_path), extractor).value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require_relative "array_value"
|
3
|
+
require_relative "array_of"
|
4
|
+
require_relative "hash_builder"
|
5
|
+
require_relative "string_value"
|
6
|
+
require_relative "value_builder"
|
7
|
+
require_relative "within"
|
8
|
+
require_relative "expression"
|
9
|
+
|
10
|
+
module Extract
|
11
|
+
class ValueBuilder < Base
|
12
|
+
def value
|
13
|
+
props = node.props
|
14
|
+
case props
|
15
|
+
when String then value_for_string
|
16
|
+
when Array then value_for_array
|
17
|
+
when Hash then value_for_hash
|
18
|
+
else
|
19
|
+
raise "Invalid kind #{props.class} (#{props})"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def value_for_hash
|
26
|
+
props = node.props
|
27
|
+
fixed_value = props[:fixed]
|
28
|
+
return fixed_value if fixed_value
|
29
|
+
return ArrayOf.new(node, extractor).value if props[:array_of]
|
30
|
+
return Within.new(node, extractor).value if props[:within]
|
31
|
+
return StringValue.new(node, extractor).value if (props.keys & %i[path attr]).any?
|
32
|
+
|
33
|
+
HashBuilder.new(node, extractor).value
|
34
|
+
end
|
35
|
+
|
36
|
+
def value_for_string
|
37
|
+
StringValue.new(Node.new({ path: node.props }, node.path), extractor).value
|
38
|
+
end
|
39
|
+
|
40
|
+
def value_for_array
|
41
|
+
ArrayValue.new(node, extractor).value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "active_support/core_ext/string"
|
3
|
+
require_relative "format/formatter"
|
4
|
+
|
5
|
+
class PathBuilder < Struct.new(:base, :parent, :tag, keyword_init: true)
|
6
|
+
def build
|
7
|
+
paths = relative_path.split("/").then do |paths|
|
8
|
+
if parent.present?
|
9
|
+
navigate_to_parent(parent, paths)
|
10
|
+
else
|
11
|
+
paths
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
paths << tag unless tag.is_a? Array
|
16
|
+
full_path = paths.flatten.compact.join("/")
|
17
|
+
"//#{full_path}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def relative_path
|
23
|
+
base.start_with?("//") ? base[2..-1] : base
|
24
|
+
end
|
25
|
+
|
26
|
+
def navigate_to_parent(parent_tag, paths)
|
27
|
+
index = path_index(parent_tag, paths)
|
28
|
+
|
29
|
+
paths[0, index + 1]
|
30
|
+
end
|
31
|
+
|
32
|
+
def path_index(tag, paths)
|
33
|
+
paths.each_with_index do |path, index|
|
34
|
+
return index if matching_tags?(path, tag)
|
35
|
+
end
|
36
|
+
0
|
37
|
+
end
|
38
|
+
|
39
|
+
def matching_tags?(item, tag)
|
40
|
+
item.gsub(/\[\d\]/, "") == tag
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class NodeParamsExtractor < Struct.new(:node)
|
45
|
+
def extract
|
46
|
+
[node.path, *node.props.values_at(:in_parent, :path, :link, :attr)]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class NodeExtractor
|
51
|
+
def initialize(xml)
|
52
|
+
@xml = Nokogiri::XML(remove_special_elements(xml), nil, Encoding::UTF_8.to_s)
|
53
|
+
@xml.remove_namespaces!
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract(path)
|
57
|
+
xml.xpath(path)
|
58
|
+
rescue StandardError
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def remove_special_elements(xml)
|
65
|
+
CGI.unescapeHTML(xml).gsub(/<br>| /, { "<br>" => "", " " => " " })
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :xml
|
69
|
+
end
|
70
|
+
|
71
|
+
class NodeValueExtractor
|
72
|
+
def initialize(node_extractor)
|
73
|
+
@node_extractor = node_extractor
|
74
|
+
end
|
75
|
+
|
76
|
+
def attr_values(path, attributes)
|
77
|
+
return attributes.map { |atr| attr_value(path, atr) } if attributes.is_a? Array
|
78
|
+
return tag_count(path) if attributes == :tag_count
|
79
|
+
|
80
|
+
attr_value(path, attributes)
|
81
|
+
end
|
82
|
+
|
83
|
+
def tag_count(path)
|
84
|
+
node_extractor.extract(path).size
|
85
|
+
end
|
86
|
+
|
87
|
+
def tag_values(base_path, paths)
|
88
|
+
return tag_value(base_path) unless paths.is_a? Array
|
89
|
+
|
90
|
+
paths.map { |path| tag_value([base_path, path].flatten.compact.join("/")) }
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_reader :node_extractor
|
96
|
+
|
97
|
+
def tag_value(path)
|
98
|
+
node_raw_value node_extractor.extract(path)
|
99
|
+
end
|
100
|
+
|
101
|
+
def attr_value(path, att)
|
102
|
+
node_raw_value node_extractor.extract(path).attribute(att)
|
103
|
+
end
|
104
|
+
|
105
|
+
def node_raw_value(node)
|
106
|
+
NodeValue.new(node).raw_value
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class NodeValue
|
111
|
+
def initialize(node)
|
112
|
+
@node = node
|
113
|
+
end
|
114
|
+
|
115
|
+
def raw_value
|
116
|
+
return "" unless node
|
117
|
+
|
118
|
+
node_size = node.try(:size).to_i
|
119
|
+
return node.map(&:text) if node_size > 1
|
120
|
+
return node.first if node_size == 1 && contains_children?
|
121
|
+
|
122
|
+
node.text
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
attr_reader :node
|
128
|
+
|
129
|
+
def contains_children?
|
130
|
+
node.first.try(:children).any? { |child| child.is_a? Nokogiri::XML::Element }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class PathManipulator
|
135
|
+
def initialize(node_value_extractor)
|
136
|
+
@node_value_extractor = node_value_extractor
|
137
|
+
end
|
138
|
+
|
139
|
+
def replace_link(original_path, link_path)
|
140
|
+
return original_path if link_path.blank?
|
141
|
+
|
142
|
+
link_value = node_value_extractor.tag_values(link_path, nil)
|
143
|
+
|
144
|
+
original_path.gsub "<link>", link_value
|
145
|
+
end
|
146
|
+
|
147
|
+
def uniq_paths(paths, uniq_by_path)
|
148
|
+
paths
|
149
|
+
.map { |path| { path: path, value: tag_value(path, uniq_by_path) } }
|
150
|
+
.then { |paths_values| remove_duplicated_paths(paths_values) }
|
151
|
+
.map { |path_value| path_value[:path] }
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
attr_reader :node_value_extractor
|
157
|
+
|
158
|
+
def tag_value(path, uniq_by_path)
|
159
|
+
node_value_extractor.tag_values([path, uniq_by_path].join("/"), "")
|
160
|
+
end
|
161
|
+
|
162
|
+
def remove_duplicated_paths(paths_values)
|
163
|
+
paths_values.delete_if.with_index do |path_value, index|
|
164
|
+
index != first_path_value_index(paths_values, path_value)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def first_path_value_index(paths_values, current_path)
|
169
|
+
paths_values.find_index { |path_value| path_value[:value] == current_path[:value] }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class Extractor
|
174
|
+
def initialize(xml, yml, modifiers)
|
175
|
+
@node_extractor = NodeExtractor.new(xml)
|
176
|
+
@node_value_extractor = NodeValueExtractor.new(node_extractor)
|
177
|
+
@path_manipulator = PathManipulator.new(node_value_extractor)
|
178
|
+
@formatter = Format::Formatter.new(yml, modifiers)
|
179
|
+
end
|
180
|
+
|
181
|
+
def extract(node)
|
182
|
+
base, parent, tag, link, attribute = NodeParamsExtractor.new(node).extract
|
183
|
+
path = PathBuilder.new(base: base, parent: parent, tag: tag).build
|
184
|
+
|
185
|
+
if link.present?
|
186
|
+
link_path = PathBuilder.new(base: base, parent: parent, tag: link).build
|
187
|
+
|
188
|
+
if tag.is_a? Array
|
189
|
+
tag = tag.map { |tag_path| replace_link(tag_path, link_path) }
|
190
|
+
else
|
191
|
+
path = replace_link(path, link_path)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
value = path_value(path, tag, attribute)
|
196
|
+
format_value(value, node.props)
|
197
|
+
end
|
198
|
+
|
199
|
+
def format_value(value, props)
|
200
|
+
formatter.format_value(value, props)
|
201
|
+
end
|
202
|
+
|
203
|
+
def replace_link(original_path, link_path)
|
204
|
+
path_manipulator.replace_link(original_path, link_path)
|
205
|
+
end
|
206
|
+
|
207
|
+
def paths_of(base_path, tag_path, link_path = nil)
|
208
|
+
path = PathBuilder.new(base: base_path, tag: tag_path).build
|
209
|
+
|
210
|
+
if link_path.present?
|
211
|
+
link_path = PathBuilder.new(base: base_path, tag: link_path).build
|
212
|
+
path = replace_link(path, link_path)
|
213
|
+
end
|
214
|
+
|
215
|
+
node = node_extractor.extract(path)
|
216
|
+
(node || []).size.times.map do |index|
|
217
|
+
"#{path}[#{index + 1}]"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def uniq_paths(paths, uniq_by_path)
|
222
|
+
return paths if uniq_by_path.blank?
|
223
|
+
|
224
|
+
path_manipulator.uniq_paths(paths, uniq_by_path)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
attr_reader :node_extractor, :node_value_extractor, :path_manipulator, :formatter
|
230
|
+
|
231
|
+
def path_value(path, tag, attribute)
|
232
|
+
return node_value_extractor.attr_values(path, attribute) if attribute.present?
|
233
|
+
|
234
|
+
node_value_extractor.tag_values(path, tag)
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative "mapper"
|
2
|
+
require_relative "modifier"
|
3
|
+
|
4
|
+
module Format
|
5
|
+
class Formatter
|
6
|
+
def initialize(yml, modifiers)
|
7
|
+
@mapper = Format::Mapper.new(yml)
|
8
|
+
@modifier = Format::Modifier.new(yml, modifiers)
|
9
|
+
end
|
10
|
+
|
11
|
+
def format_value(value, props)
|
12
|
+
modifier_prop, mapper_prop = props.values_at(:modifier, :mapper)
|
13
|
+
|
14
|
+
value
|
15
|
+
.then { |it| modifier.apply(it, modifier_prop) }
|
16
|
+
.then { |it| nullify_empty_value(it) }
|
17
|
+
.then { |it| mapper.apply(it, mapper_prop) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :modifier, :mapper
|
23
|
+
|
24
|
+
def nullify_empty_value(value)
|
25
|
+
value.blank? || value.try(:zero?) ? nil : value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Format
|
2
|
+
class Mapper
|
3
|
+
def initialize(yml)
|
4
|
+
@mappers = yml.fetch(:mappers, {})
|
5
|
+
end
|
6
|
+
|
7
|
+
def apply(raw_value, mapper_name)
|
8
|
+
return raw_value unless mapper_name
|
9
|
+
|
10
|
+
mappers.each do |name, fields|
|
11
|
+
return mapper_value(fields, raw_value) if mapper_name.to_sym == name
|
12
|
+
end
|
13
|
+
|
14
|
+
raise "Mapper not found #{mapper_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :mappers
|
20
|
+
|
21
|
+
def mapper_value(fields, value)
|
22
|
+
(fields[:options] || []).each do |option, values|
|
23
|
+
return option.to_s if [values].flatten.include?(value.to_s)
|
24
|
+
end
|
25
|
+
fields[:default] || value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Format
|
2
|
+
class Modifier
|
3
|
+
def initialize(yml, helper)
|
4
|
+
@debug = yml.fetch(:debug, false)
|
5
|
+
@helper = helper
|
6
|
+
end
|
7
|
+
|
8
|
+
def apply(raw_value, modifiers)
|
9
|
+
[modifiers].flatten.compact.reduce(raw_value) do |value, modifier|
|
10
|
+
method_name, params = modifier_props(modifier).values_at(:name, :params)
|
11
|
+
|
12
|
+
modify_value(value, method_name, params)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :helper, :debug
|
19
|
+
|
20
|
+
def modifier_props(modifier)
|
21
|
+
modifier.is_a?(String) ? { name: modifier } : modifier
|
22
|
+
end
|
23
|
+
|
24
|
+
def modify_value(value, method, params)
|
25
|
+
args = [value]
|
26
|
+
if params.present?
|
27
|
+
args = params.is_a?(Array) ? [value, *params] : [value, **params]
|
28
|
+
end
|
29
|
+
|
30
|
+
value.try(method, *params) || helper.send(method, *args)
|
31
|
+
rescue StandardError => error
|
32
|
+
raise error unless debug
|
33
|
+
|
34
|
+
"Error invoking '#{method}' with (#{args.join(',')}): #{error}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/src/node.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class Node < Struct.new(:props, :path)
|
2
|
+
def initialize(*)
|
3
|
+
super
|
4
|
+
self.path ||= ""
|
5
|
+
end
|
6
|
+
|
7
|
+
def first_only?
|
8
|
+
return unless props.is_a? Hash
|
9
|
+
|
10
|
+
props[:array_presence] == "first_only"
|
11
|
+
end
|
12
|
+
|
13
|
+
def array_of_paths
|
14
|
+
array_paths(props[:array_of])
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def array_paths(array_props)
|
20
|
+
if array_props.is_a?(Hash)
|
21
|
+
array_props.values_at(:path, :link, :uniq_by)
|
22
|
+
else
|
23
|
+
[array_props].flatten
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require_relative "src/extractor"
|
3
|
+
require_relative "src/node"
|
4
|
+
require_relative "src/extract/value_builder"
|
5
|
+
|
6
|
+
class XmlDataExtractor
|
7
|
+
def initialize(config, modifiers = nil)
|
8
|
+
@config = config
|
9
|
+
@modifiers = modifiers
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(xml)
|
13
|
+
extractor = Extractor.new(xml, config, modifiers)
|
14
|
+
schemas = config.fetch(:schemas, {})
|
15
|
+
|
16
|
+
{}.tap do |hash|
|
17
|
+
schemas.map do |key, val|
|
18
|
+
value = Extract::ValueBuilder.new(Node.new(val), extractor).value
|
19
|
+
hash[key] = value if value.present?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :config, :modifiers
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "xml_data_extractor"
|
3
|
+
spec.version = "0.1.0"
|
4
|
+
spec.authors = ["Fernando Almeida"]
|
5
|
+
spec.email = ["fernandoprsbr@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Provides a simples DSL for extracting data from XML documents"
|
8
|
+
spec.homepage = "https://github.com/monde-sistemas/xml_data_extractor"
|
9
|
+
spec.license = "MIT"
|
10
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
11
|
+
|
12
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
13
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
14
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "nokogiri"
|
26
|
+
spec.add_dependency "activesupport"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xml_data_extractor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fernando Almeida
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- fernandoprsbr@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- bin/console
|
71
|
+
- bin/setup
|
72
|
+
- lib/src/extract/array_of.rb
|
73
|
+
- lib/src/extract/array_value.rb
|
74
|
+
- lib/src/extract/base.rb
|
75
|
+
- lib/src/extract/expression.rb
|
76
|
+
- lib/src/extract/hash_builder.rb
|
77
|
+
- lib/src/extract/string_value.rb
|
78
|
+
- lib/src/extract/value_builder.rb
|
79
|
+
- lib/src/extract/within.rb
|
80
|
+
- lib/src/extractor.rb
|
81
|
+
- lib/src/format/formatter.rb
|
82
|
+
- lib/src/format/mapper.rb
|
83
|
+
- lib/src/format/modifier.rb
|
84
|
+
- lib/src/node.rb
|
85
|
+
- lib/xml_data_extractor.rb
|
86
|
+
- xml_data_extractor.gemspec
|
87
|
+
homepage: https://github.com/monde-sistemas/xml_data_extractor
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata:
|
91
|
+
homepage_uri: https://github.com/monde-sistemas/xml_data_extractor
|
92
|
+
source_code_uri: https://github.com/monde-sistemas/xml_data_extractor
|
93
|
+
changelog_uri: https://github.com/monde-sistemas/xml_data_extractor
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 2.3.0
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.0.3
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Provides a simples DSL for extracting data from XML documents
|
113
|
+
test_files: []
|