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.
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