arstotzka 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +1 -1
  3. data/Dockerfile +2 -4
  4. data/README.md +1 -1
  5. data/arstotzka.gemspec +1 -1
  6. data/config/yardstick.yml +10 -10
  7. data/lib/arstotzka.rb +12 -11
  8. data/lib/arstotzka/class_methods.rb +38 -2
  9. data/lib/arstotzka/crawler.rb +14 -21
  10. data/lib/arstotzka/fetcher.rb +24 -21
  11. data/lib/arstotzka/fetcher_builder.rb +167 -0
  12. data/lib/arstotzka/{builder.rb → method_builder.rb} +22 -49
  13. data/lib/arstotzka/options.rb +13 -6
  14. data/lib/arstotzka/reader.rb +4 -4
  15. data/lib/arstotzka/type_cast.rb +3 -2
  16. data/lib/arstotzka/version.rb +1 -1
  17. data/lib/arstotzka/wrapper.rb +1 -1
  18. data/spec/integration/yard/arstotzka/crawler_spec.rb +5 -4
  19. data/spec/integration/yard/arstotzka/fetcher_builder_spec.rb +91 -0
  20. data/spec/integration/yard/arstotzka/fetcher_spec.rb +2 -2
  21. data/spec/integration/yard/arstotzka/{builder_spec.rb → method_builder_spec.rb} +3 -2
  22. data/spec/integration/yard/arstotzka/reader_spec.rb +2 -1
  23. data/spec/lib/arstotzka/crawler_spec.rb +3 -2
  24. data/spec/lib/arstotzka/fetcher_builder_spec.rb +30 -0
  25. data/spec/lib/arstotzka/fetcher_spec.rb +13 -13
  26. data/spec/lib/arstotzka/{builder_spec.rb → method_builder_spec.rb} +2 -1
  27. data/spec/lib/arstotzka/options_spec.rb +83 -0
  28. data/spec/lib/arstotzka/reader_spec.rb +2 -1
  29. data/spec/support/models/account.rb +6 -0
  30. data/spec/support/models/arstotzka/fetcher/dummy.rb +7 -0
  31. data/spec/support/models/my_model.rb +2 -0
  32. data/spec/support/models/star.rb +13 -2
  33. data/spec/support/models/star_gazer.rb +6 -0
  34. metadata +15 -10
@@ -8,6 +8,8 @@ module Arstotzka
8
8
  #
9
9
  # @example
10
10
  # class MyModel
11
+ # include Arstotzka
12
+ #
11
13
  # attr_reader :json
12
14
  #
13
15
  # def initialize(json)
@@ -22,37 +24,37 @@ module Arstotzka
22
24
  # )
23
25
  #
24
26
  # options = Arstotzka::Options.new(full_path: 'name.first')
25
- # builder = Arstotzka::Builder.new([ :first_name ], MyModel, options)
27
+ # builder = Arstotzka::MethodBuilder.new([ :first_name ], MyModel, options)
26
28
  # builder.build
27
29
  #
28
30
  # instance.first_name # returns 'John'
29
31
  #
30
32
  # options = Arstotzka::Options.new(type: :integer)
31
- # builder = Arstotzka::Builder.new([ :age, :cars ], MyModel, options)
33
+ # builder = Arstotzka::MethodBuilder.new([ :age, 'cars' ], MyModel, options)
32
34
  # builder.build
33
35
  #
34
36
  # instance.age # returns 20
35
37
  # instance.cars # returns 2
36
38
  #
37
39
  # @see https://www.rubydoc.info/gems/sinclair Sinclair
38
- class Builder < Sinclair
40
+ class MethodBuilder < Sinclair
39
41
  include Base
40
- # Returns new instance of Arstotzka::Builder
42
+ # Returns new instance of Arstotzka::MethodBuilder
41
43
  #
42
44
  # @param attr_names [Array] list of attributes to be fetched from the hash/json
43
- # @param clazz [Class] class to receive the methods
45
+ # @param klass [Class] class to receive the methods
44
46
  # (using {https://www.rubydoc.info/gems/sinclair Sinclair})
45
47
  #
46
- # @overload initialize(attr_names, clazz, options_hash={})
48
+ # @overload initialize(attr_names, klass, options_hash={})
47
49
  # @param options_hash [Hash] hash containing extra options
48
50
  #
49
- # @overload initialize(attr_names, clazz, options)
51
+ # @overload initialize(attr_names, klass, options)
50
52
  # @param options [Arstotzka::Options] options of initialization object
51
53
  #
52
54
  # @see https://www.rubydoc.info/gems/sinclair Sinclair
53
55
  # @see Arstotzka::Options
54
- def initialize(attr_names, clazz, options = {})
55
- super(clazz)
56
+ def initialize(attr_names, klass, options = {})
57
+ super(klass)
56
58
  self.options = options
57
59
 
58
60
  @attr_names = attr_names
@@ -63,7 +65,7 @@ module Arstotzka
63
65
 
64
66
  # @private
65
67
  attr_reader :attr_names, :options
66
- delegate :json_name, :path, :full_path, :cached, to: :options
68
+ delegate :path, :full_path, :cached, to: :options
67
69
 
68
70
  # @private
69
71
  #
@@ -76,75 +78,46 @@ module Arstotzka
76
78
  end
77
79
  end
78
80
 
79
- # @private
80
- #
81
- # builds the complete key path to fetch value
82
- #
83
- # @param [String/Symbol] attribute name of the method / attribute
84
- #
85
- # @return [String] the keys path
86
- def real_path(attribute)
87
- full_path || [path, attribute].compact.join('.')
88
- end
89
-
90
- # @private
91
- #
92
- # Options needed by fetcher
93
- #
94
- # @param [String/Symbol] attribute name of the method / attribute
95
- #
96
- # @return [Hash] options
97
- #
98
- # @see Arstotzka::Fetcher
99
- def fetcher_options(attribute)
100
- options.to_h.slice(:klass, :case, :compact, :after, :type, :flatten, :default).merge(
101
- path: real_path(attribute)
102
- )
103
- end
104
-
105
81
  # @private
106
82
  #
107
83
  # Add method to the list of methods to be built
108
84
  #
109
- # @param [String/Symbol] attribute name of method / attribute
85
+ # @param [String,Symbol] attribute name of method / attribute
110
86
  #
111
87
  # @return nil
112
88
  #
113
89
  # @see Sinclair
114
90
  def add_attr(attribute)
91
+ klass.add_fetcher(attribute, options)
115
92
  add_method attribute, (cached ? cached_fetcher(attribute) : attr_fetcher(attribute)).to_s
116
93
  end
117
94
 
118
- def json_name
119
- options.json
120
- end
121
-
122
95
  # Returns the code needed to initialize fetcher
123
96
  #
124
- # @param [String/Symbol] attribute name of method / attribute
97
+ # @param [String,Symbol] attribute name of method / attribute
125
98
  #
126
- # @return [String] code
99
+ # @return [String] method code
127
100
  #
128
101
  # @see Sinclair
129
102
  # @see Artotzka::Fetcher
130
103
  def attr_fetcher(attribute)
131
104
  <<-CODE
132
- ::Arstotzka::Fetcher.new(
133
- #{json_name}, self, #{fetcher_options(attribute)}
134
- ).fetch
105
+ begin
106
+ self.class.fetcher_for(:#{attribute}, self).fetch
107
+ end
135
108
  CODE
136
109
  end
137
110
 
138
111
  # Returns the code needed to initialize a fetche and cache it
139
112
  #
140
- # @param [String/Symbol] attribute name of method / attribute
113
+ # @param [String,Symbol] attribute name of method / attribute
141
114
  #
142
- # @return [String] code
115
+ # @return [String] method code
143
116
  #
144
117
  # @see #attr_fetcher
145
118
  def cached_fetcher(attribute)
146
119
  <<-CODE
147
- @#{attribute} ||= #{attr_fetcher(attribute)}
120
+ @#{attribute} ||= #{attr_fetcher(attribute)}
148
121
  CODE
149
122
  end
150
123
  end
@@ -24,14 +24,14 @@ module Arstotzka
24
24
  # @param options [Hash] options hash
25
25
  # Options hash will be merged with {DEFAULT_OPTIONS}
26
26
  # @option options [Class] klass class to receive the methods
27
- # @option options [String/Symbol] path path of hash attributes to find the root
27
+ # @option options [String,Symbol] path path of hash attributes to find the root
28
28
  # where the attribute live (then fetching it using the attribute name)
29
- # @option options [String/Symbol] full_path: path of hash attributes to find exacttly where the
29
+ # @option options [String,Symbol] full_path: path of hash attributes to find exacttly where the
30
30
  # value live (ignoring the attribute name)
31
- # @option options [String/Symbol] json: name of the method containing the hash to be crawled
31
+ # @option options [String,Symbol] json: name of the method containing the hash to be crawled
32
32
  # @option options [Boolean] cached: flag if the result should be memorized instead of repeating
33
33
  # the crawling
34
- # @option options [String/Symbol] case: {Reader} flag definining on which case will
34
+ # @option options [String,Symbol] case: {Reader} flag definining on which case will
35
35
  # the keys be defined
36
36
  # - lower_camel: keys in the hash are lowerCamelCase
37
37
  # - upper_camel: keys in the hash are UpperCamelCase
@@ -40,11 +40,11 @@ module Arstotzka
40
40
  # removing nil results
41
41
  # @option options [Class] klass: {Fetcher} option thatwhen passed, wraps the result in an
42
42
  # instance of the given class
43
- # @option options [String/Symbol] after: {Fetcher} option with the name of the method to be
43
+ # @option options [String,Symbol] after: {Fetcher} option with the name of the method to be
44
44
  # called once the value is fetched for mapping the value
45
45
  # @option options [Boolean] flatten: {Fetcher} flag to aplly Array#flatten thus
46
46
  # avoing nested arrays
47
- # @option options [String/Symbol] type: {Fetcher} option declaring the type of the returned
47
+ # @option options [String,Symbol] type: {Fetcher} option declaring the type of the returned
48
48
  # value (to use casting)
49
49
  # - integer
50
50
  # - string
@@ -68,5 +68,12 @@ module Arstotzka
68
68
  def merge(options)
69
69
  self.class.new(to_h.merge(options))
70
70
  end
71
+
72
+ def keys
73
+ return full_path.split('.') if full_path
74
+ return [key.to_s] unless path&.present?
75
+
76
+ [path, key].compact.join('.').split('.')
77
+ end
71
78
  end
72
79
  end
@@ -43,19 +43,19 @@ module Arstotzka
43
43
  # ]
44
44
  # }
45
45
  #
46
- # reader = Arstotzka::Reader.new(keys: %w(person full_name), case: :snake)
46
+ # reader = Arstotzka::Reader.new(full_path: 'person.full_name', case: :snake)
47
47
  # reader.read(hash, 1) # returns 'John'
48
48
  #
49
49
  # @example
50
- # reader = Arstotzka::Reader.new(keys: %w(person age), case: :upper_camel)
50
+ # reader = Arstotzka::Reader.new(full_path: 'person.age', case: :upper_camel)
51
51
  # reader.read(hash, 1) # returns 23
52
52
  #
53
53
  # @example
54
- # reader = Arstotzka::Reader.new(keys: %w(person car_collection model), case: :snake)
54
+ # reader = Arstotzka::Reader.new(full_path: 'person.car_collection.model', case: :snake)
55
55
  # reader.read(hash, 1) # raises {Arstotzka::Exception::KeyNotFound}
56
56
  #
57
57
  # @example
58
- # reader = Arstotzka::Reader.new(keys: %w(person car_collection model), case: :lower_camel)
58
+ # reader = Arstotzka::Reader.new(full_path: 'person.car_collection.model', case: :lower_camel)
59
59
  # reader.read(hash, 1) # returns [
60
60
  # # { maker: 'Ford', 'model' => 'Model A' },
61
61
  # # { maker: 'BMW', 'model' => 'Jetta' }
@@ -5,7 +5,7 @@ module Arstotzka
5
5
  #
6
6
  # Concern with all the type cast methods to be used by {Wrapper}
7
7
  #
8
- # Usage of typecast is defined by the configuration of {Builder} by the usage of
8
+ # Usage of typecast is defined by the configuration of {MethodBuilder} by the usage of
9
9
  # option type
10
10
  #
11
11
  # TypeCast can also be extended to include more types
@@ -45,6 +45,7 @@ module Arstotzka
45
45
  # attr_reader :json
46
46
  #
47
47
  # expose :cars, full_path: 'cars.unit', type: :car
48
+ #
48
49
  # def initialize(hash)
49
50
  # @json = hash
50
51
  # end
@@ -73,7 +74,7 @@ module Arstotzka
73
74
  # class TypeCaster
74
75
  # include Arstotzka
75
76
  #
76
- # expose :age, type: :integer, json: :@hash
77
+ # expose :age, type: :integer, json: :@hash
77
78
  #
78
79
  # def initialize(hash)
79
80
  # @hash = hash
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arstotzka
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -13,7 +13,7 @@ module Arstotzka
13
13
  # @overload initialize(options_hash={})
14
14
  # @param options_hash [Hash] options of initialization
15
15
  # @option options_hash klass [Class] class to wrap the value
16
- # @option options_hash type [String/Symbol] type to cast the value. The
16
+ # @option options_hash type [String,Symbol] type to cast the value. The
17
17
  # possible type_cast is defined by {TypeCast}
18
18
  #
19
19
  # @overload initialize(options)
@@ -5,11 +5,12 @@ require 'spec_helper'
5
5
  describe Arstotzka::Crawler do
6
6
  describe 'yard' do
7
7
  subject(:crawler) do
8
- described_class.new(keys: keys, **options)
8
+ described_class.new(full_path: full_path, **options)
9
9
  end
10
10
 
11
- let(:options) { {} }
12
- let(:keys) { %w[person information first_name] }
11
+ let(:options) { {} }
12
+ let(:keys) { %w[person information first_name] }
13
+ let(:full_path) { keys.join('.') }
13
14
  let(:hash) do
14
15
  {
15
16
  person: {
@@ -64,7 +65,7 @@ describe Arstotzka::Crawler do
64
65
 
65
66
  context 'when block is given' do
66
67
  subject(:crawler) do
67
- described_class.new(keys: keys, **options) { |value| value&.to_sym }
68
+ described_class.new(full_path: full_path, **options) { |value| value&.to_sym }
68
69
  end
69
70
 
70
71
  it 'returns the post processed values' do
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Arstotzka::FetcherBuilder do
6
+ describe 'yard' do
7
+ describe '#build' do
8
+ subject(:builder) { described_class.new(options) }
9
+
10
+ let(:options) { Arstotzka::Options.new(options_hash) }
11
+ let(:fetcher) { builder.build(instance) }
12
+ let(:instance) { MyModel.new(hash) }
13
+
14
+ context 'with sample usage' do
15
+ let(:options_hash) { { key: :id, path: :person } }
16
+
17
+ let(:hash) do
18
+ {
19
+ person: {
20
+ id: 101
21
+ }
22
+ }
23
+ end
24
+
25
+ it 'builds a fetcher capable of fetching value' do
26
+ expect(fetcher.fetch).to eq(101)
27
+ end
28
+ end
29
+
30
+ context 'with passing full path' do
31
+ let(:options_hash) do
32
+ {
33
+ key: :player_ids,
34
+ full_path: 'teams.players.person_id',
35
+ flatten: true,
36
+ case: :snake
37
+ }
38
+ end
39
+
40
+ let(:hash) do
41
+ {
42
+ teams: [
43
+ {
44
+ name: 'Team War',
45
+ players: [
46
+ { person_id: 101 },
47
+ { person_id: 102 }
48
+ ]
49
+ }, {
50
+ name: 'Team not War',
51
+ players: [
52
+ { person_id: 201 },
53
+ { person_id: 202 }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ end
59
+
60
+ it 'builds a fetcher capable of fetching value' do
61
+ expect(fetcher.fetch).to eq([101, 102, 201, 202])
62
+ end
63
+ end
64
+
65
+ context 'when filtering the result' do
66
+ let(:instance) { StarGazer.new(hash) }
67
+ let(:hash) do
68
+ {
69
+ stars: [
70
+ { name: 'Sun', color: 'yellow' },
71
+ { name: 'HB2840-B', color: 'blue' },
72
+ { name: 'Krypton Sun', color: 'red' },
73
+ { name: 'HB0124-C', color: 'yellow' },
74
+ { name: 'HB0942-C', color: 'red' }
75
+ ]
76
+ }
77
+ end
78
+ let(:options_hash) do
79
+ { key: :stars, klass: Star, after: :only_yellow }
80
+ end
81
+
82
+ it 'builds a fetcher capable of fetching and filtering value' do
83
+ expect(fetcher.fetch).to eq([
84
+ Star.new(name: 'Sun', color: 'yellow'),
85
+ Star.new(name: 'HB0124-C', color: 'yellow')
86
+ ])
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -5,9 +5,9 @@ require 'spec_helper'
5
5
  describe Arstotzka::Fetcher do
6
6
  describe 'yard' do
7
7
  describe '#fetch' do
8
- subject(:fetcher) { described_class.new(hash, instance, **options) }
8
+ subject(:fetcher) { described_class.new(instance, **options) }
9
9
 
10
- let(:instance) { Account.new }
10
+ let(:instance) { Account.new(hash) }
11
11
  let(:options) do
12
12
  {
13
13
  path: 'transactions',
@@ -2,8 +2,10 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Arstotzka::Builder do
5
+ describe Arstotzka::MethodBuilder do
6
6
  describe 'yard' do
7
+ subject(:builder) { described_class.new(attributes, klass, options) }
8
+
7
9
  let!(:instance) { klass.new(hash) }
8
10
  let(:options) { Arstotzka::Options.new(options_hash) }
9
11
  let(:hash) do
@@ -13,7 +15,6 @@ describe Arstotzka::Builder do
13
15
  'cars' => 2.0
14
16
  }
15
17
  end
16
- let(:builder) { described_class.new(attributes, klass, options) }
17
18
 
18
19
  describe '#first_name' do
19
20
  let(:klass) { Class.new(MyModel) }
@@ -4,9 +4,10 @@ require 'spec_helper'
4
4
 
5
5
  describe Arstotzka::Reader do
6
6
  describe 'yard' do
7
- subject(:reader) { described_class.new(keys: keys, case: case_type) }
7
+ subject(:reader) { described_class.new(full_path: full_path, case: case_type) }
8
8
 
9
9
  let(:keys) { %w[person full_name] }
10
+ let(:full_path) { keys.join('.') }
10
11
  let(:case_type) { :snake }
11
12
 
12
13
  describe '#read' do
@@ -8,8 +8,9 @@ describe Arstotzka::Crawler do
8
8
  end
9
9
 
10
10
  let(:block) { proc { |v| v } }
11
- let(:keys) { '' }
12
- let(:default_options) { { keys: keys, case: :lower_camel } }
11
+ let(:keys) { [] }
12
+ let(:full_path) { keys.join('.') }
13
+ let(:default_options) { { full_path: full_path, case: :lower_camel } }
13
14
  let(:options) { {} }
14
15
  let(:json_file) { 'arstotzka.json' }
15
16
  let(:json) { load_json_fixture_file(json_file) }
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Arstotzka::FetcherBuilder do
6
+ subject(:builder) do
7
+ described_class.new options
8
+ end
9
+
10
+ let(:instance) { Arstotzka::Dummy.new(hash) }
11
+ let(:options) { { path: 'person', key: :id } }
12
+
13
+ let(:hash) do
14
+ {
15
+ person: { id: 10 }
16
+ }
17
+ end
18
+
19
+ describe '#build' do
20
+ let(:fetcher) { builder.build(instance) }
21
+
22
+ it do
23
+ expect(fetcher).to be_a(Arstotzka::Fetcher)
24
+ end
25
+
26
+ it 'builds a fetcher capable of fetching' do
27
+ expect(fetcher.fetch).to eq(10)
28
+ end
29
+ end
30
+ end