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,3 @@
1
+ module Arstotzka::Exception
2
+ class KeyNotFound < StandardError; end
3
+ end
@@ -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,15 @@
1
+ module Arstotzka::TypeCast
2
+ extend ActiveSupport::Concern
3
+
4
+ def to_integer(value)
5
+ value.to_i if value.present?
6
+ end
7
+
8
+ def to_string(value)
9
+ value.to_s
10
+ end
11
+
12
+ def to_float(value)
13
+ value.to_f if value.present?
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Arstotzka
2
+ VERSION = '1.0.0'
3
+ 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,9 @@
1
+ {
2
+ "user": {
3
+ "name": "Robert",
4
+ "full_name": "Robert Smith",
5
+ "LoginName": "robsmith",
6
+ "birthDate": "03/07/1980",
7
+ "password_reminder": null
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "user": {
3
+ "name": null
4
+ }
5
+ }
@@ -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