arstotzka 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +13 -0
  3. data/.gitignore +3 -0
  4. data/.rspec +1 -0
  5. data/Gemfile +8 -0
  6. data/Guardfile +13 -0
  7. data/LICENSE +21 -0
  8. data/README.md +179 -0
  9. data/Rakefile +7 -0
  10. data/arstotzka.gemspec +29 -0
  11. data/docker-compose.yml +18 -0
  12. data/lib/arstotzka.rb +16 -0
  13. data/lib/arstotzka/builder.rb +73 -0
  14. data/lib/arstotzka/class_methods.rb +7 -0
  15. data/lib/arstotzka/crawler.rb +45 -0
  16. data/lib/arstotzka/exception.rb +3 -0
  17. data/lib/arstotzka/fetcher.rb +50 -0
  18. data/lib/arstotzka/reader.rb +44 -0
  19. data/lib/arstotzka/type_cast.rb +15 -0
  20. data/lib/arstotzka/version.rb +3 -0
  21. data/lib/arstotzka/wrapper.rb +36 -0
  22. data/spec/fixtures/accounts.json +27 -0
  23. data/spec/fixtures/accounts_missing.json +23 -0
  24. data/spec/fixtures/arstotzka.json +38 -0
  25. data/spec/fixtures/complete_person.json +9 -0
  26. data/spec/fixtures/person.json +5 -0
  27. data/spec/integration/readme/default_spec.rb +37 -0
  28. data/spec/integration/readme/my_parser_spec.rb +86 -0
  29. data/spec/lib/arstotzka/builder_spec.rb +100 -0
  30. data/spec/lib/arstotzka/crawler_spec.rb +276 -0
  31. data/spec/lib/arstotzka/fetcher_spec.rb +96 -0
  32. data/spec/lib/arstotzka/reader_spec.rb +120 -0
  33. data/spec/lib/arstotzka/wrapper_spec.rb +121 -0
  34. data/spec/lib/arstotzka_spec.rb +129 -0
  35. data/spec/spec_helper.rb +23 -0
  36. data/spec/support/fixture_helpers.rb +19 -0
  37. data/spec/support/models.rb +5 -0
  38. data/spec/support/models/arstotzka/dummy.rb +23 -0
  39. data/spec/support/models/arstotzka/fetcher/dummy.rb +3 -0
  40. data/spec/support/models/arstotzka/type_cast.rb +6 -0
  41. data/spec/support/models/arstotzka/wrapper/dummy.rb +7 -0
  42. data/spec/support/models/game.rb +11 -0
  43. data/spec/support/models/house.rb +11 -0
  44. data/spec/support/models/my_parser.rb +28 -0
  45. data/spec/support/models/person.rb +8 -0
  46. data/spec/support/models/star.rb +7 -0
  47. data/spec/support/models/star_gazer.rb +13 -0
  48. data/spec/support/shared_examples/wrapper.rb +19 -0
  49. 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