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,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe Arstotzka::Wrapper do
4
+ let(:options) { {} }
5
+ let(:subject) { described_class.new options }
6
+ let(:hash) { { a: 1 } }
7
+
8
+ describe '#wrap' do
9
+ let(:value) { hash }
10
+ let(:result) { subject.wrap(value) }
11
+
12
+ context 'with default options' do
13
+ it 'does not change the json value' do
14
+ expect(result).to eq(value)
15
+ end
16
+
17
+ context 'with an array as value' do
18
+ let(:value) { [hash] }
19
+
20
+ it 'does not change the array' do
21
+ expect(result).to eq(value)
22
+ end
23
+ end
24
+ end
25
+
26
+ context 'with clazz otpion' do
27
+ let(:options) { { clazz: OpenStruct } }
28
+
29
+ it 'creates new instance from given class' do
30
+ expect(result).to be_a(OpenStruct)
31
+ expect(result.a).to eq(hash[:a])
32
+ end
33
+
34
+ context 'with an array as value' do
35
+ let(:value) { [hash] }
36
+
37
+ it 'returns an array of objects of the given class' do
38
+ expect(result).to be_a(Array)
39
+ expect(result.first).to be_a(OpenStruct)
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'with type otpion' do
45
+ let(:type) { :integer }
46
+ let(:value) { '1' }
47
+ let(:options) { { type: type } }
48
+ let(:cast) { result }
49
+
50
+ it_behaves_like 'casts basic types'
51
+
52
+ context 'when processing an array' do
53
+ let(:value) { [1.0] }
54
+ let(:cast) { result.first }
55
+
56
+ it_behaves_like 'casts basic types'
57
+
58
+ it do
59
+ expect(result).to be_a(Array)
60
+ end
61
+ end
62
+
63
+ context 'with nil value' do
64
+ let(:value) { nil }
65
+
66
+ it do
67
+ expect(result).to be_nil
68
+ end
69
+
70
+ context 'when passing clazz parameter' do
71
+ let(:options) { { type: type, clazz: Arstotzka::Wrapper::Dummy } }
72
+
73
+ it do
74
+ expect(result).to be_nil
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'with blank value' do
80
+ let(:value) { '' }
81
+
82
+ it_behaves_like 'a result that is type cast', {
83
+ integer: NilClass,
84
+ float: NilClass,
85
+ string: String
86
+ }
87
+
88
+ context 'when passing clazz parameter' do
89
+ let(:options) { { type: type, clazz: Arstotzka::Wrapper::Dummy } }
90
+
91
+ it_behaves_like 'a result that is type cast', {
92
+ integer: NilClass,
93
+ float: NilClass,
94
+ string: Arstotzka::Wrapper::Dummy
95
+ }
96
+ end
97
+ end
98
+
99
+ context 'when passing clazz parameter' do
100
+ let(:value) { 1 }
101
+ let(:options) { { type: type, clazz: Arstotzka::Wrapper::Dummy } }
102
+ let(:cast) { result.value }
103
+
104
+ it_behaves_like 'casts basic types'
105
+
106
+ it 'wraps the result inside the given class' do
107
+ expect(result).to be_a(Arstotzka::Wrapper::Dummy)
108
+ end
109
+ end
110
+
111
+ context 'with none for type' do
112
+ let(:type) { :none }
113
+ let(:value) { 1.0 }
114
+
115
+ it 'does not cast the value' do
116
+ expect(result).to eq(value)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+
3
+ describe Arstotzka do
4
+ let(:dummy) { Arstotzka::Dummy.new(json) }
5
+ let(:json) { load_json_fixture_file('arstotzka.json') }
6
+ let(:value) { dummy.public_send(attribute) }
7
+
8
+ context 'when parser is configured with no options' do
9
+ let(:attribute) { :id }
10
+
11
+ it 'retrieves attribute from base json' do
12
+ expect(value).to eq(json['id'])
13
+ end
14
+ end
15
+
16
+ context 'when parser is configured with a path' do
17
+ let(:attribute) { :name }
18
+
19
+ it 'retrieves attribute from base json' do
20
+ expect(value).to eq(json['user']['name'])
21
+ end
22
+ end
23
+
24
+ context 'when configuring full path' do
25
+ let(:attribute) { :father_name }
26
+
27
+ it 'returns nil' do
28
+ expect(value).to eq(json['father']['name'])
29
+ end
30
+ end
31
+
32
+ context 'when caching the value' do
33
+ let(:attribute) { :age }
34
+ let!(:old_value) { json['age'] }
35
+ before do
36
+ dummy.age
37
+ json['age'] = old_value + 100
38
+ end
39
+
40
+ it 'returns cached value' do
41
+ expect(value).to eq(old_value)
42
+ end
43
+ end
44
+
45
+ context 'when wrapping it with a class' do
46
+ let(:attribute) { :house }
47
+
48
+ it 'returns an onject wrap' do
49
+ expect(value).to be_a(House)
50
+ end
51
+
52
+ it 'creates the object with the given json' do
53
+ expect(value.age).to eq(json['house']['age'])
54
+ expect(value.value).to eq(json['house']['value'])
55
+ expect(value.floors).to eq(json['house']['floors'])
56
+ end
57
+
58
+ context 'when dealing with an array' do
59
+ let(:attribute) { :games }
60
+
61
+ it 'returns an array of json wrapped' do
62
+ expect(value).to be_a(Array)
63
+ value.each do |object|
64
+ expect(object).to be_a(Game)
65
+ end
66
+ end
67
+
68
+ context 'when dealing with multiple level arrays' do
69
+ let(:attribute) { :games }
70
+ before do
71
+ json['games'].map! { |j| [j] }
72
+ end
73
+
74
+ it 'returns an array of json wrapped' do
75
+ expect(value).to be_a(Array)
76
+ value.each do |object|
77
+ expect(object).to be_a(Array)
78
+ object.each do |game|
79
+ expect(game).to be_a(Game)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ context 'when wrapping it with a class and caching' do
88
+ let(:attribute) { :old_house }
89
+ let!(:old_value) { json['oldHouse'] }
90
+ before do
91
+ dummy.old_house
92
+ json['oldHouse'] = {}
93
+ end
94
+
95
+ it 'returns an onject wrap' do
96
+ expect(value).to be_a(House)
97
+ end
98
+
99
+ it 'creates the object with the given json' do
100
+ expect(value.age).not_to eq(json['oldHouse']['age'])
101
+ expect(value.age).to eq(old_value['age'])
102
+ expect(value.value).not_to eq(json['oldHouse']['value'])
103
+ expect(value.value).to eq(old_value['value'])
104
+ expect(value.floors).not_to eq(json['oldHouse']['floors'])
105
+ expect(value.floors).to eq(old_value['floors'])
106
+ end
107
+ end
108
+
109
+ context 'when passing an after filter' do
110
+ let(:attribute) { :games_filtered }
111
+
112
+ it 'applies the filter after parsing the json' do
113
+ expect(value.map(&:publisher)).not_to include('sega')
114
+ end
115
+ end
116
+
117
+ context 'when casting the result' do
118
+ class Arstotzka::Dummy
119
+ expose :float_value, type: :float
120
+ end
121
+
122
+ let(:json) { { floatValue: '1' } }
123
+ let(:attribute) { :float_value }
124
+
125
+ it do
126
+ expect(value).to be_a(Float)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,23 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter 'spec/support/models/'
4
+ end
5
+
6
+ require 'pry-nav'
7
+ require 'arstotzka'
8
+ require 'safe_attribute_assignment'
9
+
10
+ support_files = File.expand_path("spec/support/**/*.rb")
11
+ Dir[support_files].sort.each { |file| require file }
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+ config.filter_run_excluding :integration unless ENV['ALL']
18
+
19
+ config.order = 'random'
20
+
21
+ config.before do
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_support'
2
+
3
+ module FixtureHelpers
4
+ def load_fixture_file(filename)
5
+ File.read (['./spec/', 'fixtures', filename].join('/'))
6
+ end
7
+
8
+ def load_json_fixture_file(filename)
9
+ cached_json_fixture_file(filename)
10
+ end
11
+
12
+ private
13
+
14
+ def cached_json_fixture_file(filename)
15
+ ActiveSupport::JSON.decode(load_fixture_file(filename))
16
+ end
17
+ end
18
+
19
+ RSpec.configuration.include FixtureHelpers
@@ -0,0 +1,5 @@
1
+ models = File.expand_path("spec/support/models/*.rb")
2
+ Dir[models].sort.each do |file|
3
+ autoload file.gsub(/.*\/(.*)\..*/, '\1').camelize.to_sym, file
4
+ end
5
+
@@ -0,0 +1,23 @@
1
+ class Arstotzka::Dummy
2
+ include Arstotzka
3
+ attr_reader :json
4
+
5
+ expose :id
6
+ expose :name, path: 'user'
7
+ expose :father_name, full_path: 'father.name'
8
+ expose :age, cached: true
9
+ expose :house, class: ::House
10
+ expose :old_house, class: ::House, cached: true
11
+ expose :games, class: ::Game
12
+ expose :games_filtered, class: ::Game, after: :filter_games, full_path: 'games'
13
+
14
+ def initialize(json)
15
+ @json = json
16
+ end
17
+
18
+ def filter_games(games)
19
+ games.select do |g|
20
+ g.publisher != 'sega'
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ class Arstotzka::Fetcher::Dummy
2
+ end
3
+
@@ -0,0 +1,6 @@
1
+ module Arstotzka::TypeCast
2
+ def to_money_float(value)
3
+ value.gsub(/\$ */, '').to_f
4
+ end
5
+ end
6
+
@@ -0,0 +1,7 @@
1
+ class Arstotzka::Wrapper::Dummy
2
+ attr_reader :value
3
+ def initialize(value)
4
+ @value = value
5
+ end
6
+ end
7
+
@@ -0,0 +1,11 @@
1
+ class Game
2
+ include Arstotzka
3
+ include SafeAttributeAssignment
4
+ attr_reader :json
5
+
6
+ expose :name, :publisher
7
+
8
+ def initialize(json)
9
+ @json = json
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class House
2
+ include Arstotzka
3
+ include ::SafeAttributeAssignment
4
+ attr_reader :json
5
+
6
+ expose :age, :value, :floors
7
+
8
+ def initialize(json)
9
+ @json = json
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ class MyParser
2
+ include Arstotzka
3
+
4
+ expose :id
5
+ expose :name, :age, path: :person
6
+ expose :total_money, full_path: 'accounts.balance', after: :sum,
7
+ cached: true, type: :money_float
8
+ expose :total_owed, full_path: 'loans.value', after: :sum,
9
+ cached: true, type: :money_float
10
+
11
+ attr_reader :json
12
+
13
+ def initialize(json = {})
14
+ @json = json
15
+ end
16
+
17
+ private
18
+
19
+ def sum(balances)
20
+ balances.sum if balances
21
+ end
22
+
23
+ models = File.expand_path("spec/support/models/my_parser/*.rb")
24
+ Dir[models].each do |file|
25
+ autoload file.gsub(/.*\/(.*)\..*/, '\1').camelize.to_sym, file
26
+ end
27
+ end
28
+
@@ -0,0 +1,8 @@
1
+ class Person
2
+ attr_reader :name
3
+
4
+ def initialize(name)
5
+ @name = name
6
+ end
7
+ end
8
+
@@ -0,0 +1,7 @@
1
+ class Star
2
+ attr_reader :name
3
+
4
+ def initialize(name:)
5
+ @name = name
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ class StarGazer
2
+ include Arstotzka
3
+
4
+ expose :favorite_star, full_path: 'universe.star',
5
+ default: { name: 'Sun' }, class: ::Star
6
+
7
+ attr_reader :json
8
+
9
+ def initialize(json = {})
10
+ @json = json
11
+ end
12
+ end
13
+
@@ -0,0 +1,19 @@
1
+ shared_context 'a result that is type cast' do |types|
2
+ types.each do |type, clazz|
3
+ context "with #{type} type" do
4
+ let(:type) { type }
5
+
6
+ it do
7
+ expect(cast).to be_a(clazz)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ shared_context 'casts basic types' do
14
+ it_behaves_like 'a result that is type cast', {
15
+ integer: Integer,
16
+ float: Float,
17
+ string: String
18
+ }
19
+ end