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,50 @@
|
|
1
|
+
class Arstotzka::Fetcher
|
2
|
+
include Sinclair::OptionsParser
|
3
|
+
|
4
|
+
attr_reader :path, :json, :instance
|
5
|
+
|
6
|
+
delegate :after, :flatten, to: :options_object
|
7
|
+
delegate :wrap, to: :wrapper
|
8
|
+
|
9
|
+
def initialize(json, instance, path:, **options)
|
10
|
+
@path = path.to_s.split('.')
|
11
|
+
@json = json
|
12
|
+
@instance = instance
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch
|
17
|
+
value = crawler.value(json)
|
18
|
+
value.flatten! if flatten && value.respond_to?(:flatten!)
|
19
|
+
value = instance.send(after, value) if after
|
20
|
+
value
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def crawler
|
26
|
+
@crawler ||= buidl_crawler
|
27
|
+
end
|
28
|
+
|
29
|
+
def buidl_crawler
|
30
|
+
Arstotzka::Crawler.new(crawler_options) do |value|
|
31
|
+
wrap(value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def crawler_options
|
36
|
+
options.slice(:case_type, :compact, :default).merge(path: path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def wrapper
|
40
|
+
@wrapper ||= build_wrapper
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_wrapper
|
44
|
+
Arstotzka::Wrapper.new(wrapper_options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def wrapper_options
|
48
|
+
options.slice(:clazz, :type)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Arstotzka
|
2
|
+
class Reader
|
3
|
+
attr_reader :path, :case_type
|
4
|
+
|
5
|
+
def initialize(path:, case_type:)
|
6
|
+
@case_type = case_type
|
7
|
+
@path = path.map(&self.method(:change_case))
|
8
|
+
end
|
9
|
+
|
10
|
+
def read(json, index)
|
11
|
+
key = path[index]
|
12
|
+
|
13
|
+
check_key!(json, key)
|
14
|
+
|
15
|
+
json.key?(key) ? json[key] : json[key.to_sym]
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_ended?(index)
|
19
|
+
index >= path.size
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def check_key!(json, key)
|
25
|
+
return if has_key?(json, key)
|
26
|
+
raise Exception::KeyNotFound
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_key?(json, key)
|
30
|
+
json&.key?(key) || json&.key?(key.to_sym)
|
31
|
+
end
|
32
|
+
|
33
|
+
def change_case(key)
|
34
|
+
case case_type
|
35
|
+
when :lower_camel
|
36
|
+
key.camelize(:lower)
|
37
|
+
when :upper_camel
|
38
|
+
key.camelize(:upper)
|
39
|
+
when :snake
|
40
|
+
key.underscore
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Arstotzka::Wrapper
|
2
|
+
include Arstotzka::TypeCast
|
3
|
+
|
4
|
+
attr_reader :clazz, :type
|
5
|
+
|
6
|
+
def initialize(clazz: nil, type: nil)
|
7
|
+
@clazz = clazz
|
8
|
+
@type = type
|
9
|
+
end
|
10
|
+
|
11
|
+
def wrap(value)
|
12
|
+
return wrap_array(value) if value.is_a?(Array)
|
13
|
+
wrap_element(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def wrap_element(value)
|
19
|
+
value = cast(value) if has_type? && !value.nil?
|
20
|
+
return if value.nil?
|
21
|
+
|
22
|
+
clazz ? clazz.new(value) : value
|
23
|
+
end
|
24
|
+
|
25
|
+
def wrap_array(array)
|
26
|
+
array.map { |v| wrap v }
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_type?
|
30
|
+
type.present? && type != :none
|
31
|
+
end
|
32
|
+
|
33
|
+
def cast(value)
|
34
|
+
public_send("to_#{type}", value)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"banks": [
|
3
|
+
{
|
4
|
+
"name": "bank_1",
|
5
|
+
"accounts": [
|
6
|
+
{
|
7
|
+
"type": "savings",
|
8
|
+
"balance": 1000.00
|
9
|
+
}, {
|
10
|
+
"type": "checking",
|
11
|
+
"balance": 1500.00
|
12
|
+
}
|
13
|
+
]
|
14
|
+
}, {
|
15
|
+
"name": "bank_2",
|
16
|
+
"accounts": [
|
17
|
+
{
|
18
|
+
"type": "savings",
|
19
|
+
"balance": 50.00
|
20
|
+
}, {
|
21
|
+
"type": "checking",
|
22
|
+
"balance": -500.00
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
{
|
2
|
+
"banks": [
|
3
|
+
{
|
4
|
+
"name": "bank_1",
|
5
|
+
"accounts": [
|
6
|
+
{
|
7
|
+
"type": "savings",
|
8
|
+
"balance": 1000.00
|
9
|
+
}, {
|
10
|
+
"type": "checking"
|
11
|
+
}, {
|
12
|
+
"type": "investiment",
|
13
|
+
"balance": null
|
14
|
+
}
|
15
|
+
]
|
16
|
+
}, {
|
17
|
+
"name": "bank_2",
|
18
|
+
"accounts": null
|
19
|
+
}, {
|
20
|
+
"name": "bank_3"
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
{
|
2
|
+
"id": 100,
|
3
|
+
"user": {
|
4
|
+
"name": "usuario"
|
5
|
+
},
|
6
|
+
"age": 20,
|
7
|
+
"father": {
|
8
|
+
"name": "father full name"
|
9
|
+
},
|
10
|
+
"hasMoney": true,
|
11
|
+
"house": {
|
12
|
+
"age": 20,
|
13
|
+
"value": 1000,
|
14
|
+
"floors": 3
|
15
|
+
},
|
16
|
+
"oldHouse": {
|
17
|
+
"age": 30,
|
18
|
+
"value": 500,
|
19
|
+
"floors": 1
|
20
|
+
},
|
21
|
+
"games": [
|
22
|
+
{
|
23
|
+
"name": "zelda",
|
24
|
+
"publisher": "nintendo"
|
25
|
+
}, {
|
26
|
+
"name": "mario",
|
27
|
+
"publisher": "nintendo"
|
28
|
+
}, {
|
29
|
+
"name": "sonic",
|
30
|
+
"publisher": "sega"
|
31
|
+
}
|
32
|
+
],
|
33
|
+
"animals":[
|
34
|
+
{"race": {"species": {"name": "European squid"}}},
|
35
|
+
{"race": {"species": {"name": "Macaque monkey"}}},
|
36
|
+
{"race": {"species": {"name": "Mexican redknee tarantula"}}}
|
37
|
+
]
|
38
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'default option' do
|
4
|
+
subject do
|
5
|
+
StarGazer.new(hash).favorite_star
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:hash) { {} }
|
9
|
+
|
10
|
+
context 'when node is not found' do
|
11
|
+
it 'returns the default before wrapping' do
|
12
|
+
expect(subject.name).to eq('Sun')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'wraps the returned value in a class' do
|
16
|
+
expect(subject).to be_a(Star)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when node is not missing' do
|
21
|
+
let(:hash) do
|
22
|
+
{
|
23
|
+
universe: {
|
24
|
+
star: { name: 'Antares' }
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns the value before wrapping' do
|
30
|
+
expect(subject.name).to eq('Antares')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'wraps the returned value in a class' do
|
34
|
+
expect(subject).to be_a(Star)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MyParser do
|
4
|
+
subject { described_class.new(hash) }
|
5
|
+
|
6
|
+
let(:hash) do
|
7
|
+
{
|
8
|
+
id: 10,
|
9
|
+
person: {
|
10
|
+
name: 'Robert',
|
11
|
+
age: 22
|
12
|
+
},
|
13
|
+
accounts: [
|
14
|
+
{ balance: '$ 1000.50', type: 'checking' },
|
15
|
+
{ balance: '$ 150.10', type: 'savings' },
|
16
|
+
{ balance: '$ -100.24', type: 'checking' }
|
17
|
+
],
|
18
|
+
loans: [
|
19
|
+
{ value: '$ 300.50', bank: 'the_bank' },
|
20
|
+
{ value: '$ 150.10', type: 'the_other_bank' },
|
21
|
+
{ value: '$ 100.24', type: 'the_same_bank' }
|
22
|
+
]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'id' do
|
27
|
+
it 'returns the parsed id' do
|
28
|
+
expect(subject.id).to eq(10)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'name' do
|
33
|
+
it 'returns the parsed name' do
|
34
|
+
expect(subject.name).to eq('Robert')
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when person is missing' do
|
38
|
+
subject { described_class.new }
|
39
|
+
|
40
|
+
it do
|
41
|
+
expect { subject.name }.not_to raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it do
|
45
|
+
expect(subject.name).to be_nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'age' do
|
51
|
+
it do
|
52
|
+
expect(subject.age).to be_a(Integer)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns the parsed age' do
|
56
|
+
expect(subject.age).to eq(22)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#total_money' do
|
61
|
+
it do
|
62
|
+
expect(subject.total_money).to be_a(Float)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'summs all the balance in the accounts' do
|
66
|
+
expect(subject.total_money).to eq(1050.36)
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when there is a node missing' do
|
70
|
+
let(:hash) { {} }
|
71
|
+
it 'returns nil' do
|
72
|
+
expect(subject.total_money).to be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#total_owed' do
|
78
|
+
it do
|
79
|
+
expect(subject.total_owed).to be_a(Float)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'summs all the balance in the accounts' do
|
83
|
+
expect(subject.total_owed).to eq(550.84)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Arstotzka::Builder do
|
4
|
+
let(:clazz) do
|
5
|
+
Class.new.tap do |c|
|
6
|
+
c.send(:attr_reader, :json)
|
7
|
+
c.send(:define_method, :initialize) do |json={}|
|
8
|
+
@json = json
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:options) { {} }
|
14
|
+
let(:name) { 'Robert' }
|
15
|
+
let(:attr_name) { :name }
|
16
|
+
let(:attr_names) { [ attr_name ] }
|
17
|
+
let(:json) { {} }
|
18
|
+
let(:instance) { clazz.new(json) }
|
19
|
+
|
20
|
+
subject do
|
21
|
+
described_class.new(attr_names, clazz, **options)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#build' do
|
25
|
+
it 'adds the reader' do
|
26
|
+
expect do
|
27
|
+
subject.build
|
28
|
+
end.to change { clazz.new.respond_to?(attr_name) }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'after building' do
|
32
|
+
before { subject.build }
|
33
|
+
|
34
|
+
context 'when building several attributes' do
|
35
|
+
let(:attr_names) { [ :id, :name, :age ] }
|
36
|
+
|
37
|
+
it 'adds all the readers' do
|
38
|
+
attr_names.each do |attr|
|
39
|
+
expect(instance).to respond_to(attr)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'fetches safelly empty jsons' do
|
44
|
+
expect(instance.name).to be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when json has the property' do
|
48
|
+
let(:json) { { name: name } }
|
49
|
+
|
50
|
+
it 'fetches the value' do
|
51
|
+
expect(instance.name).to eq(name)
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'but key is a string' do
|
55
|
+
let(:json) { { 'name' => name } }
|
56
|
+
|
57
|
+
it 'fetches the value' do
|
58
|
+
expect(instance.name).to eq(name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when value is deep within the json' do
|
65
|
+
let(:json) { { user: { name: name } } }
|
66
|
+
|
67
|
+
context 'when defining a path' do
|
68
|
+
let(:options) { { path: 'user' } }
|
69
|
+
|
70
|
+
it 'fetches the value within the json' do
|
71
|
+
expect(instance.name).to eq(name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when defining a fullpath' do
|
76
|
+
let(:options) { { full_path: 'user.name' } }
|
77
|
+
let(:attr_name) { :the_name }
|
78
|
+
|
79
|
+
it 'fetches the value within the json' do
|
80
|
+
expect(instance.the_name).to eq(name)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when wrapping with a class' do
|
86
|
+
let(:json) { { person: name } }
|
87
|
+
let(:options) { { class: Person } }
|
88
|
+
let(:attr_name) { :person }
|
89
|
+
|
90
|
+
it do
|
91
|
+
expect(instance.person).to be_a(Person)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'fills the new instance with the information fetched' do
|
95
|
+
expect(instance.person.name).to eq(name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|