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