arstotzka 1.0.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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +13 -0
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/Guardfile +13 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/Rakefile +7 -0
- data/arstotzka.gemspec +29 -0
- data/docker-compose.yml +18 -0
- data/lib/arstotzka.rb +16 -0
- data/lib/arstotzka/builder.rb +73 -0
- data/lib/arstotzka/class_methods.rb +7 -0
- data/lib/arstotzka/crawler.rb +45 -0
- data/lib/arstotzka/exception.rb +3 -0
- data/lib/arstotzka/fetcher.rb +50 -0
- data/lib/arstotzka/reader.rb +44 -0
- data/lib/arstotzka/type_cast.rb +15 -0
- data/lib/arstotzka/version.rb +3 -0
- data/lib/arstotzka/wrapper.rb +36 -0
- data/spec/fixtures/accounts.json +27 -0
- data/spec/fixtures/accounts_missing.json +23 -0
- data/spec/fixtures/arstotzka.json +38 -0
- data/spec/fixtures/complete_person.json +9 -0
- data/spec/fixtures/person.json +5 -0
- data/spec/integration/readme/default_spec.rb +37 -0
- data/spec/integration/readme/my_parser_spec.rb +86 -0
- data/spec/lib/arstotzka/builder_spec.rb +100 -0
- data/spec/lib/arstotzka/crawler_spec.rb +276 -0
- data/spec/lib/arstotzka/fetcher_spec.rb +96 -0
- data/spec/lib/arstotzka/reader_spec.rb +120 -0
- data/spec/lib/arstotzka/wrapper_spec.rb +121 -0
- data/spec/lib/arstotzka_spec.rb +129 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/fixture_helpers.rb +19 -0
- data/spec/support/models.rb +5 -0
- data/spec/support/models/arstotzka/dummy.rb +23 -0
- data/spec/support/models/arstotzka/fetcher/dummy.rb +3 -0
- data/spec/support/models/arstotzka/type_cast.rb +6 -0
- data/spec/support/models/arstotzka/wrapper/dummy.rb +7 -0
- data/spec/support/models/game.rb +11 -0
- data/spec/support/models/house.rb +11 -0
- data/spec/support/models/my_parser.rb +28 -0
- data/spec/support/models/person.rb +8 -0
- data/spec/support/models/star.rb +7 -0
- data/spec/support/models/star_gazer.rb +13 -0
- data/spec/support/shared_examples/wrapper.rb +19 -0
- metadata +229 -0
@@ -0,0 +1,276 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Arstotzka::Crawler do
|
4
|
+
let(:subject) do
|
5
|
+
described_class.new default_options.merge(options), &block
|
6
|
+
end
|
7
|
+
let(:block) { proc { |v| v } }
|
8
|
+
let(:path) { '' }
|
9
|
+
let(:default_options) { { path: path, case_type: :lower_camel} }
|
10
|
+
let(:options) { {} }
|
11
|
+
let(:json_file) { 'arstotzka.json' }
|
12
|
+
let(:json) { load_json_fixture_file(json_file) }
|
13
|
+
let(:value) { subject.value(json) }
|
14
|
+
|
15
|
+
context 'when parsing with a path' do
|
16
|
+
let(:path) { %w(user name) }
|
17
|
+
|
18
|
+
it 'retrieves attribute from base json' do
|
19
|
+
expect(value).to eq(json['user']['name'])
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when calling twice' do
|
23
|
+
before { subject.value(json) }
|
24
|
+
|
25
|
+
it 'can still crawl' do
|
26
|
+
expect(value).to eq(json['user']['name'])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'crawler finds a nil attribute' do
|
32
|
+
let(:path) { %w(car model) }
|
33
|
+
|
34
|
+
it 'returns nil' do
|
35
|
+
expect(value).to be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it do
|
39
|
+
expect { value }.not_to raise_error
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when there is an array of arrays' do
|
44
|
+
let(:json_file) { 'accounts.json' }
|
45
|
+
let(:path) { %w(banks accounts balance) }
|
46
|
+
|
47
|
+
it 'returns the values as array of arrays' do
|
48
|
+
expect(value).to eq([[1000.0, 1500.0], [50.0, -500.0]])
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when there is a missing node' do
|
52
|
+
let(:json_file) { 'accounts_missing.json' }
|
53
|
+
|
54
|
+
it 'returns the missing values as nil' do
|
55
|
+
expect(value).to eq([[1000.0, nil, nil], nil, nil])
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when setting a default' do
|
59
|
+
let(:options) { { default: 10 } }
|
60
|
+
|
61
|
+
it 'returns the missing values as default' do
|
62
|
+
expect(value).to eq([[1000.0, 10, nil], 10, 10])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when setting compact' do
|
67
|
+
let(:options) { { compact: true } }
|
68
|
+
it 'returns the missing values as nil' do
|
69
|
+
expect(value).to eq([[1000.0]])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when json is empty' do
|
76
|
+
let(:json) { nil }
|
77
|
+
let(:path) { %w(car model) }
|
78
|
+
|
79
|
+
it 'returns nil' do
|
80
|
+
expect(value).to be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it do
|
84
|
+
expect { value }.not_to raise_error
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with an snake case path' do
|
89
|
+
let(:path) { ['has_money'] }
|
90
|
+
|
91
|
+
it 'returns camel cased value' do
|
92
|
+
expect(value).to eq(json['hasMoney'])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when dealing with json inside arrays' do
|
97
|
+
let(:path) { %w(animals race species name)}
|
98
|
+
let(:expected) do
|
99
|
+
['European squid', 'Macaque monkey', 'Mexican redknee tarantula']
|
100
|
+
end
|
101
|
+
|
102
|
+
it do
|
103
|
+
expect(value).to be_a(Array)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'parses them mapping arrays as sub parse' do
|
107
|
+
expect(value).to eq(expected)
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'when there are nil values' do
|
111
|
+
context 'with compact option as false' do
|
112
|
+
let(:options) { { compact: false } }
|
113
|
+
before do
|
114
|
+
json["animals"].last['race'] = nil
|
115
|
+
end
|
116
|
+
let(:expected) do
|
117
|
+
['European squid', 'Macaque monkey', nil]
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'eliminate nil values' do
|
121
|
+
expect(value).to eq(expected)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'with compact option' do
|
126
|
+
let(:options) { { compact: true } }
|
127
|
+
before do
|
128
|
+
json["animals"].last['race'] = nil
|
129
|
+
end
|
130
|
+
let(:expected) do
|
131
|
+
['European squid', 'Macaque monkey']
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'eliminate nil values' do
|
135
|
+
expect(value).to eq(expected)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'with default option' do
|
142
|
+
let(:default_value) { 'NotFound' }
|
143
|
+
let(:options) { { default: default_value } }
|
144
|
+
let(:path) { %w(projects name) }
|
145
|
+
|
146
|
+
context 'when there is a key missing' do
|
147
|
+
it 'returns the default value' do
|
148
|
+
expect(value).to eq(default_value)
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'when wrapping it with a class' do
|
152
|
+
let(:block) { proc { |v| Person.new(v) } }
|
153
|
+
|
154
|
+
it 'wrap it with the class' do
|
155
|
+
expect(value).to be_a(Person)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'wraps the default value' do
|
159
|
+
expect(value.name).to eq(default_value)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'when the key is not missing but the value is nil' do
|
165
|
+
let(:json_file) { 'person.json' }
|
166
|
+
let(:path) { %w(user name) }
|
167
|
+
|
168
|
+
it { expect(value).to be_nil }
|
169
|
+
|
170
|
+
context 'when wrapping it with a class' do
|
171
|
+
let(:block) { proc { |v| Person.new(v) } }
|
172
|
+
|
173
|
+
it 'wrap it with the class' do
|
174
|
+
expect(value).to be_a(Person)
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'wraps the default value' do
|
178
|
+
expect(value.name).to be_nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'when the key last key is missing but the value is nil' do
|
184
|
+
let(:json_file) { 'person.json' }
|
185
|
+
let(:path) { %w(user nick_name) }
|
186
|
+
|
187
|
+
it 'returns the default value' do
|
188
|
+
expect(value).to eq(default_value)
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'when wrapping it with a class' do
|
192
|
+
let(:block) { proc { |v| Person.new(v) } }
|
193
|
+
|
194
|
+
it 'wrap it with the class' do
|
195
|
+
expect(value).to be_a(Person)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'wraps the default value' do
|
199
|
+
expect(value.name).to eq(default_value)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context 'when the node is missing but default has the same node' do
|
205
|
+
let(:default_value) { { node: { value: 1 } } }
|
206
|
+
let(:path) { %w(node node node) }
|
207
|
+
let(:json) { {} }
|
208
|
+
|
209
|
+
it 'does not crawl through default value' do
|
210
|
+
expect(value).to eq(default_value)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context 'when using a snake case' do
|
216
|
+
let(:json) { { snake_cased: 'snake', snakeCased: 'Camel' }.stringify_keys }
|
217
|
+
let(:path) { [ 'snake_cased' ] }
|
218
|
+
let(:options) { { case_type: :snake } }
|
219
|
+
|
220
|
+
it 'fetches from snake cased fields' do
|
221
|
+
expect(value).to eq('snake')
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
context 'when using a upper camel case' do
|
226
|
+
let(:json) { { UpperCase: 'upper', upperCase: 'lower' }.stringify_keys }
|
227
|
+
let(:path) { [ 'upper_case' ] }
|
228
|
+
let(:options) { { case_type: :upper_camel } }
|
229
|
+
|
230
|
+
it 'fetches from upper camel cased fields' do
|
231
|
+
expect(value).to eq('upper')
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'when using a symbol keys' do
|
236
|
+
let(:json) { load_json_fixture_file('arstotzka.json').symbolize_keys }
|
237
|
+
let(:path) { [ 'id' ] }
|
238
|
+
|
239
|
+
it 'fetches from symbol keys' do
|
240
|
+
expect(value).to eq(json[:id])
|
241
|
+
end
|
242
|
+
|
243
|
+
context 'crawler finds a nil attribute' do
|
244
|
+
let(:path) { %w(car model) }
|
245
|
+
|
246
|
+
it 'returns nil' do
|
247
|
+
expect(value).to be_nil
|
248
|
+
end
|
249
|
+
|
250
|
+
it do
|
251
|
+
expect { value }.not_to raise_error
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context 'when using key with false value' do
|
257
|
+
let(:path) { ['has_money'] }
|
258
|
+
before do
|
259
|
+
json['hasMoney'] = false
|
260
|
+
end
|
261
|
+
|
262
|
+
context 'with string keys' do
|
263
|
+
it { expect(value).to be_falsey }
|
264
|
+
it { expect(value).not_to be_nil }
|
265
|
+
end
|
266
|
+
|
267
|
+
context 'with symbol keys' do
|
268
|
+
before do
|
269
|
+
json.symbolize_keys!
|
270
|
+
end
|
271
|
+
|
272
|
+
it { expect(value).to be_falsey }
|
273
|
+
it { expect(value).not_to be_nil }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Arstotzka::Fetcher do
|
4
|
+
let(:subject) do
|
5
|
+
described_class.new json, instance, options.merge(path: path)
|
6
|
+
end
|
7
|
+
let(:path) { '' }
|
8
|
+
let(:instance) { Arstotzka::Fetcher::Dummy.new }
|
9
|
+
let(:json) { load_json_fixture_file('arstotzka.json') }
|
10
|
+
let(:value) { subject.fetch }
|
11
|
+
|
12
|
+
context 'when fetching with no options' do
|
13
|
+
let(:options) { {} }
|
14
|
+
let(:path) { 'id' }
|
15
|
+
|
16
|
+
it 'retrieves attribute from base json' do
|
17
|
+
expect(value).to eq(json['id'])
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when calling the method twice' do
|
21
|
+
before do
|
22
|
+
subject.fetch
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'retrieves attribute from base json' do
|
26
|
+
expect(value).to eq(json['id'])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when changing json value' do
|
31
|
+
let!(:old_value) { json['id'] }
|
32
|
+
before do
|
33
|
+
subject.fetch
|
34
|
+
json['id'] = 200
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'retrieves the new value' do
|
38
|
+
expect(value).not_to eq(old_value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'flatten options' do
|
44
|
+
let(:json) { [[[1,2],[3,4]],[[5,6],[7,8]]] }
|
45
|
+
|
46
|
+
context 'when flatten option is true' do
|
47
|
+
let(:options) { { flatten: true } }
|
48
|
+
|
49
|
+
it 'returns the fetched value flattened' do
|
50
|
+
expect(subject.fetch).to eq((1..8).to_a)
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when value is not an array' do
|
54
|
+
let(:json) { 1 }
|
55
|
+
|
56
|
+
it 'returns the fetched value flattened' do
|
57
|
+
expect(subject.fetch).to eq(1)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when flatten option is false' do
|
63
|
+
let(:options) { { flatten: false } }
|
64
|
+
|
65
|
+
it 'returns the fetched value non flattened' do
|
66
|
+
expect(subject.fetch).to eq(json)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'after option' do
|
72
|
+
let(:instance) { MyParser.new(json) }
|
73
|
+
let(:json) { [ 100, 250, -25] }
|
74
|
+
let(:options) { { after: :sum } }
|
75
|
+
|
76
|
+
it 'applies after call ' do
|
77
|
+
expect(subject.fetch).to eq(325)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'clazz options' do
|
82
|
+
let(:path) { 'name' }
|
83
|
+
let(:name) { 'Robert' }
|
84
|
+
let(:json) { { name: name } }
|
85
|
+
let(:options) { { clazz: wrapper } }
|
86
|
+
let(:wrapper) { Person }
|
87
|
+
|
88
|
+
it 'wraps the result in an object' do
|
89
|
+
expect(subject.fetch).to be_a(wrapper)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'sets the wrapper with the fetched value' do
|
93
|
+
expect(subject.fetch.name).to eq(name)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples 'reader fetchin value' do
|
4
|
+
it do
|
5
|
+
expect { subject.read(json, index) }.not_to raise_error
|
6
|
+
end
|
7
|
+
|
8
|
+
it do
|
9
|
+
expect(subject.read(json, index)).not_to be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns the evaluated value' do
|
13
|
+
expect(subject.read(json, index)).to eq(expected)
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'and the json has symbolized_keys' do
|
17
|
+
it 'returns the evaluated value' do
|
18
|
+
expect(subject.read(sym_json, index)).to eq(expected)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Arstotzka::Reader do
|
24
|
+
subject do
|
25
|
+
described_class.new(path: path, case_type: case_type)
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:path) { %w(user full_name) }
|
29
|
+
let(:json_file) { 'complete_person.json' }
|
30
|
+
let(:full_json) { load_json_fixture_file(json_file) }
|
31
|
+
let(:json) { full_json }
|
32
|
+
let(:sym_json) { json.symbolize_keys }
|
33
|
+
let(:case_type) { :snake }
|
34
|
+
let(:index) { 0 }
|
35
|
+
|
36
|
+
describe '#read' do
|
37
|
+
context 'when the key is found' do
|
38
|
+
let(:expected) { json['user'] }
|
39
|
+
|
40
|
+
it_behaves_like 'reader fetchin value'
|
41
|
+
|
42
|
+
context 'when the path case is changed' do
|
43
|
+
let(:json) { full_json['user'] }
|
44
|
+
let(:index) { 1 }
|
45
|
+
|
46
|
+
context 'to snake_case' do
|
47
|
+
let(:path) { %w(user FullName) }
|
48
|
+
let(:expected) { json['full_name'] }
|
49
|
+
|
50
|
+
it_behaves_like 'reader fetchin value'
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'to upper_camel' do
|
54
|
+
let(:case_type) { :upper_camel }
|
55
|
+
let(:path) { %w(user login_name) }
|
56
|
+
let(:expected) { json['LoginName'] }
|
57
|
+
|
58
|
+
it_behaves_like 'reader fetchin value'
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'to lower_camel' do
|
62
|
+
let(:case_type) { :lower_camel }
|
63
|
+
let(:path) { %w(user birth_date) }
|
64
|
+
let(:expected) { json['birthDate'] }
|
65
|
+
|
66
|
+
it_behaves_like 'reader fetchin value'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when key is found but value is null' do
|
71
|
+
let(:json) { full_json['user'] }
|
72
|
+
let(:index) { 1 }
|
73
|
+
let(:path) { %w(user password_reminder) }
|
74
|
+
|
75
|
+
it do
|
76
|
+
expect(subject.read(json, index)).to be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'but keys are symbol' do
|
80
|
+
it do
|
81
|
+
expect(subject.read(sym_json, index)).to be_nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when json has both string and symble' do
|
87
|
+
let(:path) { %w(key) }
|
88
|
+
let(:json) { { key: 'symbol', 'key' => 'string' } }
|
89
|
+
|
90
|
+
it 'fetches the string key first' do
|
91
|
+
expect(subject.read(json, index)).to eq('string')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when the key is missing' do
|
97
|
+
let(:path) { %w(age) }
|
98
|
+
|
99
|
+
it do
|
100
|
+
expect do
|
101
|
+
subject.read(json, index)
|
102
|
+
end.to raise_error(Arstotzka::Exception::KeyNotFound)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe 'is_ended?' do
|
108
|
+
context 'when index is within path' do
|
109
|
+
let(:index) { 1 }
|
110
|
+
|
111
|
+
it { expect(subject.is_ended?(index)).to be_falsey }
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when index is outside path' do
|
115
|
+
let(:index) { 2 }
|
116
|
+
|
117
|
+
it { expect(subject.is_ended?(index)).to be_truthy }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|