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