arstotzka 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|