arstotzka 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +19 -8
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/.rubocop_todo.yml +6 -3
  6. data/.yardopts +1 -0
  7. data/Dockerfile +8 -0
  8. data/README.md +2 -3
  9. data/Rakefile +2 -0
  10. data/arstotzka.gemspec +11 -8
  11. data/config/yardstick.rb +13 -0
  12. data/config/yardstick.yml +69 -0
  13. data/docker-compose.yml +14 -9
  14. data/lib/arstotzka/builder.rb +72 -0
  15. data/lib/arstotzka/class_methods.rb +9 -2
  16. data/lib/arstotzka/crawler.rb +56 -5
  17. data/lib/arstotzka/fetcher.rb +43 -14
  18. data/lib/arstotzka/reader.rb +44 -1
  19. data/lib/arstotzka/type_cast.rb +114 -1
  20. data/lib/arstotzka/version.rb +1 -1
  21. data/lib/arstotzka/wrapper.rb +47 -2
  22. data/spec/integration/readme/arstotzka_spec.rb +41 -0
  23. data/spec/integration/readme/my_parser_spec.rb +14 -13
  24. data/spec/integration/yard/arstotzka/crawler_spec.rb +9 -9
  25. data/spec/integration/yard/arstotzka/fetcher_spec.rb +4 -4
  26. data/spec/integration/yard/arstotzka/reader_spec.rb +7 -7
  27. data/spec/integration/yard/arstotzka/type_cast_spec.rb +55 -0
  28. data/spec/integration/yard/arstotzka/wrapper_spec.rb +30 -14
  29. data/spec/integration/yard/arstotzka_spec.rb +9 -9
  30. data/spec/lib/arstotzka/builder_spec.rb +18 -16
  31. data/spec/lib/arstotzka/crawler_spec.rb +21 -13
  32. data/spec/lib/arstotzka/fetcher_spec.rb +12 -10
  33. data/spec/lib/arstotzka/reader_spec.rb +16 -16
  34. data/spec/lib/arstotzka/wrapper_spec.rb +8 -2
  35. data/spec/lib/arstotzka_spec.rb +23 -23
  36. data/spec/support/models/car.rb +10 -0
  37. data/spec/support/models/car_collector.rb +20 -0
  38. data/spec/support/models/request.rb +9 -0
  39. data/spec/support/models/type_caster.rb +13 -0
  40. data/spec/support/shared_examples/wrapper.rb +2 -2
  41. metadata +65 -23
  42. data/spec/integration/readme/default_spec.rb +0 -39
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arstotzka
4
+ # @api private
5
+ #
4
6
  # Reads a value from a hash using the path as list of keys
5
7
  class Reader
8
+ # Creates a new instance of Reader
9
+ #
6
10
  # @param path [Array] path of keys broken down as array
7
11
  # @param case_type [Symbol] Case of the keys
8
12
  # - lower_camel: keys in the hash are lowerCamelCase
9
13
  # - upper_camel: keys in the hash are UpperCamelCase
10
14
  # - snake: keys in the hash are snake_case
15
+ #
16
+ # @return [Aristotzka::Reader]
11
17
  def initialize(path:, case_type:)
12
18
  @case_type = case_type
13
19
  @path = path.map(&method(:change_case))
@@ -18,7 +24,7 @@ module Arstotzka
18
24
  # @param hash [Hash] hash to be read
19
25
  # @param index [Integer] Index of the key (in path) to be used
20
26
  #
21
- # @return Object The value fetched from the hash
27
+ # @return [Object] The value fetched from the hash
22
28
  #
23
29
  # @example
24
30
  # hash = {
@@ -55,6 +61,8 @@ module Arstotzka
55
61
  hash.key?(key) ? hash[key] : hash[key.to_sym]
56
62
  end
57
63
 
64
+ # @private
65
+ #
58
66
  # Checks if index is within path range
59
67
  #
60
68
  # @example
@@ -67,17 +75,52 @@ module Arstotzka
67
75
 
68
76
  private
69
77
 
78
+ # @private
70
79
  attr_reader :path, :case_type
71
80
 
81
+ # @private
82
+ #
83
+ # Checks if a hash contains or not the key
84
+ #
85
+ # if the key is not found, an execption is raised
86
+ #
87
+ # @raise Arstotzka::Exception::KeyNotFound
88
+ #
89
+ # @return [NilClass]
90
+ #
91
+ # @see #key?
72
92
  def check_key!(hash, key)
73
93
  return if key?(hash, key)
74
94
  raise Exception::KeyNotFound
75
95
  end
76
96
 
97
+ # @private
98
+ #
99
+ # Checks if a hash contains or not the key
100
+ #
101
+ # The check first happens using String key and,
102
+ # in case of not found, searches as symbol
103
+ #
104
+ # @param [Hash] hash Hash where the key will be found
105
+ # @param [String] key The key to be checked
106
+ #
107
+ # @return [Boolean]
108
+ #
109
+ # @see #check_key!
77
110
  def key?(hash, key)
78
111
  hash&.key?(key) || hash&.key?(key.to_sym)
79
112
  end
80
113
 
114
+ # @private
115
+ #
116
+ # Transforms the key to have the correct case
117
+ #
118
+ # the possible case_types (instance attribute) are
119
+ # - lower_camel: for cammel case with first letter lowercase
120
+ # - upper_camel: for cammel case with first letter uppercase
121
+ # - snake: for snake case
122
+ #
123
+ # @param [String] key the key to be transformed
81
124
  def change_case(key)
82
125
  case case_type
83
126
  when :lower_camel
@@ -1,27 +1,140 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arstotzka
4
+ # @api public
5
+ #
4
6
  # Concern with all the type cast methods to be used by {Wrapper}
5
7
  #
6
8
  # Usage of typecast is defined by the configuration of {Builder} by the usage of
7
9
  # option type
10
+ #
11
+ # TypeCast can also be extended to include more types
12
+ #
13
+ # Supported types:
14
+ # - integer
15
+ # - string
16
+ # - float
17
+ #
18
+ # @example (see #to_integer)
19
+ #
20
+ # @example (see #to_string)
21
+ #
22
+ # @example (see #to_float)
23
+ #
24
+ # @example Extending typecast
25
+ # class Car
26
+ # attr_reader :model, :maker
27
+ #
28
+ # def initialize(model:, maker:)
29
+ # @model = model
30
+ # @maker = maker
31
+ # end
32
+ # end
33
+ #
34
+ # module Arstotzka
35
+ # module TypeCast
36
+ # def to_car(hash)
37
+ # Car.new(hash.symbolize_keys)
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # class CarCollector
43
+ # include Arstotzka
44
+ #
45
+ # attr_reader :json
46
+ #
47
+ # expose :cars, full_path: 'cars.unit', type: :car
48
+ # def initialize(hash)
49
+ # @json = hash
50
+ # end
51
+ # end
52
+ #
53
+ # hash = {
54
+ # cars: [{
55
+ # unit: { model: 'fox', maker: 'volkswagen' }
56
+ # }, {
57
+ # unit: { 'model' => 'focus', 'maker' => 'ford' }
58
+ # }]
59
+ # }
60
+ #
61
+ # model = CarCollector.new(hash)
62
+ #
63
+ # model.cars # returns [<Car>, <Car>]
64
+ # model.cars.map(&:model) # returns ['fox', 'focus']
8
65
  module TypeCast
9
66
  extend ActiveSupport::Concern
10
67
 
11
- # converts a value to integer
68
+ # Converts a value to integer
69
+ #
12
70
  # @return [Integer]
71
+ #
72
+ # @example Casting to Integer
73
+ # class TypeCaster
74
+ # include Arstotzka
75
+ #
76
+ # expose :age, type: :integer, json: :@hash
77
+ #
78
+ # def initialize(hash)
79
+ # @hash = hash
80
+ # end
81
+ # end
82
+ #
83
+ # hash = {
84
+ # age: '10',
85
+ # }
86
+ #
87
+ # TypeCaseter.new(age: '10').age
13
88
  def to_integer(value)
14
89
  value.to_i if value.present?
15
90
  end
16
91
 
17
92
  # converts value to string
93
+ #
18
94
  # @return [String]
95
+ #
96
+ # @example Casting to String
97
+ # class TypeCaster
98
+ # include Arstotzka
99
+ #
100
+ # expose :payload, type: :string, json: :@hash
101
+ #
102
+ # def initialize(hash)
103
+ # @hash = hash
104
+ # end
105
+ # end
106
+ #
107
+ # hash = {
108
+ # payload: { 'key' => 'value' },
109
+ # }
110
+ #
111
+ # model.TypeCaseter.new(hash)
112
+ #
113
+ # model.payload # returns '{"key"=>"value"}'
19
114
  def to_string(value)
20
115
  value.to_s
21
116
  end
22
117
 
23
118
  # converts value to float
119
+ #
24
120
  # @return [Float]
121
+ #
122
+ # @example Casting to Float
123
+ # class TypeCaster
124
+ # include Arstotzka
125
+ #
126
+ # expose :price, type: :float, json: :@hash
127
+ #
128
+ # def initialize(hash)
129
+ # @hash = hash
130
+ # end
131
+ # end
132
+ #
133
+ # hash = {
134
+ # price: '1.75'
135
+ # }
136
+ #
137
+ # TypeCaseter.new(price: '1.75').price # returns 1.75
25
138
  def to_float(value)
26
139
  value.to_f if value.present?
27
140
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arstotzka
4
- VERSION = '1.0.3'
4
+ VERSION = '1.0.4'
5
5
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arstotzka
4
+ # @api private
5
+ #
4
6
  # Class responsible for wrapping / parsing a value fetched
5
7
  class Wrapper
6
8
  include Arstotzka::TypeCast
7
9
 
10
+ # Returns a new instance of Wrapper
11
+ #
8
12
  # @param clazz [Class] class to wrap the value
9
13
  # @param type [String/Symbol] type to cast the value. The
10
14
  # possible type_cast is defined by {TypeCast}
@@ -15,7 +19,9 @@ module Arstotzka
15
19
 
16
20
  # wrap a value
17
21
  #
18
- # @example
22
+ # @return [Object]
23
+ #
24
+ # @example Wrapping in a class
19
25
  # class Person
20
26
  # attr_reader :name
21
27
  #
@@ -27,9 +33,23 @@ module Arstotzka
27
33
  # wrapper = Arstotzka::Wrapper.new(clazz: Person)
28
34
  # wrapper.wrap('John') # retruns Person.new('John')
29
35
  #
30
- # @example
36
+ # @example Casting type
31
37
  # wrapper = Arstotzka::Wrapper.new(type: :integer)
32
38
  # wrapper.wrap(['10', '20', '30']) # retruns [10, 20, 30]
39
+ #
40
+ # @example Casting and Wrapping
41
+ # class Request
42
+ # attr_reader :payload
43
+ #
44
+ # def initialize(payload)
45
+ # @payload = payload
46
+ # end
47
+ # end
48
+ #
49
+ # wrapper = Arstotzka::Wrapper.new(type: :string, clazz: Request)
50
+ # request = wrapper.wrap(value)
51
+ #
52
+ # request.payload # returns '{"key"=>"value"}'
33
53
  def wrap(value)
34
54
  return wrap_array(value) if value.is_a?(Array)
35
55
  wrap_element(value)
@@ -37,8 +57,14 @@ module Arstotzka
37
57
 
38
58
  private
39
59
 
60
+ # @private
40
61
  attr_reader :clazz, :type
41
62
 
63
+ # @private
64
+ #
65
+ # Wraps an element with a class and perform typecasting
66
+ #
67
+ # @return [Object]
42
68
  def wrap_element(value)
43
69
  value = cast(value) if type? && !value.nil?
44
70
  return if value.nil?
@@ -46,14 +72,33 @@ module Arstotzka
46
72
  clazz ? clazz.new(value) : value
47
73
  end
48
74
 
75
+ # @private
76
+ #
77
+ # Wraps each element of the array
78
+ #
79
+ # @see #wrap_element
80
+ #
81
+ # @return [Arra]
49
82
  def wrap_array(array)
50
83
  array.map { |v| wrap v }
51
84
  end
52
85
 
86
+ # @private
87
+ #
88
+ # Check if type was given
89
+ #
90
+ # @return [Boolean]
53
91
  def type?
54
92
  type.present? && type != :none
55
93
  end
56
94
 
95
+ # @private
96
+ #
97
+ # Performs type casting
98
+ #
99
+ # @see Arstotzka::TypeCaster
100
+ #
101
+ # @return [Object]
57
102
  def cast(value)
58
103
  public_send("to_#{type}", value)
59
104
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Arstotzka do
6
+ describe 'default option' do
7
+ subject(:star_gazer) do
8
+ StarGazer.new(hash).favorite_star
9
+ end
10
+
11
+ let(:hash) { {} }
12
+
13
+ context 'when node is not found' do
14
+ it 'returns the default before wrapping' do
15
+ expect(star_gazer.name).to eq('Sun')
16
+ end
17
+
18
+ it 'wraps the returned value in a class' do
19
+ expect(star_gazer).to be_a(Star)
20
+ end
21
+ end
22
+
23
+ context 'when node is not missing' do
24
+ let(:hash) do
25
+ {
26
+ universe: {
27
+ star: { name: 'Antares' }
28
+ }
29
+ }
30
+ end
31
+
32
+ it 'returns the value before wrapping' do
33
+ expect(star_gazer.name).to eq('Antares')
34
+ end
35
+
36
+ it 'wraps the returned value in a class' do
37
+ expect(star_gazer).to be_a(Star)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,7 +3,7 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe MyParser do
6
- subject { described_class.new(hash) }
6
+ subject(:parser) { described_class.new(hash) }
7
7
 
8
8
  let(:hash) do
9
9
  {
@@ -27,62 +27,63 @@ describe MyParser do
27
27
 
28
28
  describe 'id' do
29
29
  it 'returns the parsed id' do
30
- expect(subject.id).to eq(10)
30
+ expect(parser.id).to eq(10)
31
31
  end
32
32
  end
33
33
 
34
34
  describe 'name' do
35
35
  it 'returns the parsed name' do
36
- expect(subject.name).to eq('Robert')
36
+ expect(parser.name).to eq('Robert')
37
37
  end
38
38
 
39
39
  context 'when person is missing' do
40
- subject { described_class.new }
40
+ subject(:parser) { described_class.new }
41
41
 
42
42
  it do
43
- expect { subject.name }.not_to raise_error
43
+ expect { parser.name }.not_to raise_error
44
44
  end
45
45
 
46
46
  it do
47
- expect(subject.name).to be_nil
47
+ expect(parser.name).to be_nil
48
48
  end
49
49
  end
50
50
  end
51
51
 
52
52
  describe 'age' do
53
53
  it do
54
- expect(subject.age).to be_a(Integer)
54
+ expect(parser.age).to be_a(Integer)
55
55
  end
56
56
 
57
57
  it 'returns the parsed age' do
58
- expect(subject.age).to eq(22)
58
+ expect(parser.age).to eq(22)
59
59
  end
60
60
  end
61
61
 
62
62
  describe '#total_money' do
63
63
  it do
64
- expect(subject.total_money).to be_a(Float)
64
+ expect(parser.total_money).to be_a(Float)
65
65
  end
66
66
 
67
67
  it 'summs all the balance in the accounts' do
68
- expect(subject.total_money).to eq(1050.36)
68
+ expect(parser.total_money).to eq(1050.36)
69
69
  end
70
70
 
71
71
  context 'when there is a node missing' do
72
72
  let(:hash) { {} }
73
+
73
74
  it 'returns nil' do
74
- expect(subject.total_money).to be_nil
75
+ expect(parser.total_money).to be_nil
75
76
  end
76
77
  end
77
78
  end
78
79
 
79
80
  describe '#total_owed' do
80
81
  it do
81
- expect(subject.total_owed).to be_a(Float)
82
+ expect(parser.total_owed).to be_a(Float)
82
83
  end
83
84
 
84
85
  it 'summs all the balance in the accounts' do
85
- expect(subject.total_owed).to eq(550.84)
86
+ expect(parser.total_owed).to eq(550.84)
86
87
  end
87
88
  end
88
89
  end
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  describe Arstotzka::Crawler do
6
6
  describe 'yard' do
7
- subject do
7
+ subject(:crawler) do
8
8
  described_class.new(path: path, **options)
9
9
  end
10
10
 
@@ -21,13 +21,13 @@ describe Arstotzka::Crawler do
21
21
  end
22
22
 
23
23
  it 'crawls to find the value' do
24
- expect(subject.value(hash)).to eq('John')
24
+ expect(crawler.value(hash)).to eq('John')
25
25
  end
26
26
 
27
27
  describe '#value' do
28
28
  context 'when hash contains the path' do
29
29
  it 'crawls to find the value' do
30
- expect(subject.value(hash)).to eq('John')
30
+ expect(crawler.value(hash)).to eq('John')
31
31
  end
32
32
  end
33
33
 
@@ -51,24 +51,24 @@ describe Arstotzka::Crawler do
51
51
  end
52
52
 
53
53
  it 'crawls to find the value' do
54
- expect(subject.value(hash)).to eq([['Rakhar']])
54
+ expect(crawler.value(hash)).to eq([['Rakhar']])
55
55
  end
56
56
 
57
- context 'and we set a default value' do
57
+ context 'with default value' do
58
58
  let(:options) { { compact: true, case_type: :snake, default: 'NO HERO' } }
59
59
 
60
60
  it 'return default value for missed keys' do
61
- expect(subject.value(hash)).to eq([['NO HERO', 'Rakhar'], 'NO HERO'])
61
+ expect(crawler.value(hash)).to eq([['NO HERO', 'Rakhar'], 'NO HERO'])
62
62
  end
63
63
  end
64
64
 
65
- context 'and we give a block' do
66
- subject do
65
+ context 'when block is given' do
66
+ subject(:crawler) do
67
67
  described_class.new(path: path, **options) { |value| value&.to_sym }
68
68
  end
69
69
 
70
70
  it 'returns the post processed values' do
71
- expect(subject.value(hash)).to eq([[:Rakhar]])
71
+ expect(crawler.value(hash)).to eq([[:Rakhar]])
72
72
  end
73
73
  end
74
74
  end
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  describe Arstotzka::Fetcher do
6
6
  describe 'yard' do
7
7
  describe '#fetch' do
8
- subject { described_class.new(hash, instance, path: path, **options) }
8
+ subject(:fetcher) { described_class.new(hash, instance, path: path, **options) }
9
9
 
10
10
  let(:instance) { Account.new }
11
11
  let(:path) { 'transactions' }
@@ -31,11 +31,11 @@ describe Arstotzka::Fetcher do
31
31
 
32
32
  describe 'incoming transactions' do
33
33
  it 'returns only the income payments' do
34
- expect(subject.fetch.count).to eq(2)
34
+ expect(fetcher.fetch.count).to eq(2)
35
35
  end
36
36
 
37
37
  it 'returns Transactions' do
38
- expect(subject.fetch.map(&:class).uniq).to eq([Transaction])
38
+ expect(fetcher.fetch.map(&:class).uniq).to eq([Transaction])
39
39
  end
40
40
 
41
41
  it 'returns results wrapped in Transactions' do
@@ -43,7 +43,7 @@ describe Arstotzka::Fetcher do
43
43
  Transaction.new(value: 1000.53, type: 'income'),
44
44
  Transaction.new(value: 50.23, type: 'income')
45
45
  ]
46
- expect(subject.fetch).to eq(expected)
46
+ expect(fetcher.fetch).to eq(expected)
47
47
  end
48
48
  end
49
49
  end
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  describe Arstotzka::Reader do
6
6
  describe 'yard' do
7
- subject { described_class.new(path: path, case_type: case_type) }
7
+ subject(:reader) { described_class.new(path: path, case_type: case_type) }
8
8
 
9
9
  let(:path) { %w[person full_name] }
10
10
  let(:case_type) { :snake }
@@ -23,7 +23,7 @@ describe Arstotzka::Reader do
23
23
 
24
24
  context 'when using snake_case' do
25
25
  it 'fetches the value using snake case key' do
26
- expect(subject.read(hash, 1)).to eq('John')
26
+ expect(reader.read(hash, 1)).to eq('John')
27
27
  end
28
28
 
29
29
  context 'when key is missing' do
@@ -31,7 +31,7 @@ describe Arstotzka::Reader do
31
31
 
32
32
  it do
33
33
  expect do
34
- subject.read(hash, 1)
34
+ reader.read(hash, 1)
35
35
  end.to raise_error(Arstotzka::Exception::KeyNotFound)
36
36
  end
37
37
  end
@@ -46,7 +46,7 @@ describe Arstotzka::Reader do
46
46
  { maker: 'Ford', 'model' => 'Model A' },
47
47
  { maker: 'BMW', 'model' => 'Jetta' }
48
48
  ]
49
- expect(subject.read(hash, 1)).to eq(expected)
49
+ expect(reader.read(hash, 1)).to eq(expected)
50
50
  end
51
51
  end
52
52
 
@@ -55,7 +55,7 @@ describe Arstotzka::Reader do
55
55
  let(:path) { %w[person age] }
56
56
 
57
57
  it 'fetches the value using uper camel case key' do
58
- expect(subject.read(hash, 1)).to eq(23)
58
+ expect(reader.read(hash, 1)).to eq(23)
59
59
  end
60
60
  end
61
61
  end
@@ -63,13 +63,13 @@ describe Arstotzka::Reader do
63
63
  describe '#ended?' do
64
64
  context 'when the fetches have not ended' do
65
65
  it do
66
- expect(subject.ended?(1)).to be_falsey
66
+ expect(reader).not_to be_ended(1)
67
67
  end
68
68
  end
69
69
 
70
70
  context 'when the fetches have ended' do
71
71
  it do
72
- expect(subject.ended?(2)).to be_truthy
72
+ expect(reader).to be_ended(2)
73
73
  end
74
74
  end
75
75
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Arstotzka::TypeCast do
4
+ subject(:model) { TypeCaster.new(hash) }
5
+
6
+ let(:hash) do
7
+ {
8
+ age: '10',
9
+ payload: { 'key' => 'value' },
10
+ price: '1.75'
11
+ }
12
+ end
13
+
14
+ describe 'yard' do
15
+ describe 'integer' do
16
+ it 'converts string to integer' do
17
+ expect(model.age).to eq(10)
18
+ end
19
+ end
20
+
21
+ describe 'string' do
22
+ it 'converts value to string' do
23
+ expect(model.payload).to eq('{"key"=>"value"}')
24
+ end
25
+ end
26
+
27
+ describe 'float' do
28
+ it 'converts value to string' do
29
+ expect(model.price).to eq(1.75)
30
+ end
31
+ end
32
+
33
+ describe 'extending' do
34
+ subject(:model) { CarCollector.new(hash) }
35
+
36
+ let(:hash) do
37
+ {
38
+ cars: [{
39
+ unit: { model: 'fox', maker: 'volkswagen' }
40
+ }, {
41
+ unit: { 'model' => 'focus', 'maker' => 'ford' }
42
+ }]
43
+ }
44
+ end
45
+
46
+ it 'converts each unit to be a car' do
47
+ expect(model.cars.first).to be_a(Car)
48
+ end
49
+
50
+ it 'converts using the given hash' do
51
+ expect(model.cars.map(&:model)).to eq(%w[fox focus])
52
+ end
53
+ end
54
+ end
55
+ end