arstotzka 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +11 -0
  5. data/.rubocop_todo.yml +20 -0
  6. data/Gemfile +3 -2
  7. data/Guardfile +3 -2
  8. data/README.md +24 -5
  9. data/Rakefile +5 -3
  10. data/arstotzka.gemspec +18 -15
  11. data/arstotzka.jpg +0 -0
  12. data/lib/arstotzka.rb +2 -0
  13. data/lib/arstotzka/builder.rb +109 -47
  14. data/lib/arstotzka/class_methods.rb +34 -0
  15. data/lib/arstotzka/crawler.rb +86 -11
  16. data/lib/arstotzka/exception.rb +7 -2
  17. data/lib/arstotzka/fetcher.rb +93 -36
  18. data/lib/arstotzka/reader.rb +59 -11
  19. data/lib/arstotzka/type_cast.rb +24 -10
  20. data/lib/arstotzka/version.rb +3 -1
  21. data/lib/arstotzka/wrapper.rb +59 -34
  22. data/spec/integration/readme/default_spec.rb +2 -0
  23. data/spec/integration/readme/my_parser_spec.rb +2 -0
  24. data/spec/integration/yard/arstotzka/builder_spec.rb +63 -0
  25. data/spec/integration/yard/arstotzka/class_methods_spec.rb +49 -0
  26. data/spec/integration/yard/arstotzka/crawler_spec.rb +77 -0
  27. data/spec/integration/yard/arstotzka/fetcher_spec.rb +51 -0
  28. data/spec/integration/yard/arstotzka/reader_spec.rb +77 -0
  29. data/spec/integration/yard/arstotzka/wrapper_spec.rb +29 -0
  30. data/spec/lib/arstotzka/builder_spec.rb +6 -4
  31. data/spec/lib/arstotzka/crawler_spec.rb +30 -17
  32. data/spec/lib/arstotzka/fetcher_spec.rb +4 -2
  33. data/spec/lib/arstotzka/reader_spec.rb +13 -11
  34. data/spec/lib/arstotzka/wrapper_spec.rb +10 -10
  35. data/spec/lib/arstotzka_spec.rb +12 -8
  36. data/spec/spec_helper.rb +6 -2
  37. data/spec/support/fixture_helpers.rb +4 -2
  38. data/spec/support/models.rb +4 -3
  39. data/spec/support/models/account.rb +9 -0
  40. data/spec/support/models/arstotzka/dummy.rb +21 -17
  41. data/spec/support/models/arstotzka/fetcher/dummy.rb +7 -2
  42. data/spec/support/models/arstotzka/type_cast.rb +7 -4
  43. data/spec/support/models/arstotzka/wrapper/dummy.rb +10 -5
  44. data/spec/support/models/game.rb +2 -0
  45. data/spec/support/models/house.rb +2 -0
  46. data/spec/support/models/my_model.rb +9 -0
  47. data/spec/support/models/my_parser.rb +6 -5
  48. data/spec/support/models/person.rb +7 -1
  49. data/spec/support/models/star.rb +2 -0
  50. data/spec/support/models/star_gazer.rb +3 -2
  51. data/spec/support/models/transaction.rb +19 -0
  52. data/spec/support/shared_examples/wrapper.rb +6 -5
  53. metadata +64 -15
@@ -1,3 +1,8 @@
1
- module Arstotzka::Exception
2
- class KeyNotFound < StandardError; end
1
+ # frozen_string_literal: true
2
+
3
+ module Arstotzka
4
+ module Exception
5
+ # Exception raised when a key in the hash is not found
6
+ class KeyNotFound < StandardError; end
7
+ end
3
8
  end
@@ -1,50 +1,107 @@
1
- class Arstotzka::Fetcher
2
- include Sinclair::OptionsParser
1
+ # frozen_string_literal: true
3
2
 
4
- attr_reader :path, :json, :instance
3
+ module Arstotzka
4
+ # Class responsible for orquestrating the fetch value from the hash
5
+ # and post-processing it
6
+ class Fetcher
7
+ include Sinclair::OptionsParser
5
8
 
6
- delegate :after, :flatten, to: :options_object
7
- delegate :wrap, to: :wrapper
9
+ # @param hash [Hash] Hash to be crawled for value
10
+ # @param instance [Object] object whose methods will be called after for processing
11
+ # @param path [String/Symbol] complete path for fetching the value from hash
12
+ # @param options [Hash] options that will be passed to {Crawler}, {Wrapper} and {Reader}
13
+ def initialize(hash, instance, path:, **options)
14
+ @path = path.to_s.split('.')
15
+ @hash = hash
16
+ @instance = instance
17
+ @options = options
18
+ end
8
19
 
9
- def initialize(json, instance, path:, **options)
10
- @path = path.to_s.split('.')
11
- @json = json
12
- @instance = instance
13
- @options = options
14
- end
20
+ # Crawls the hash for the value, applying then the final transformations on the final
21
+ # result (collection not value)
22
+ #
23
+ # @example
24
+ # class Transaction
25
+ # attr_reader :value, :type
26
+ #
27
+ # def initialize(value:, type:)
28
+ # @value = value
29
+ # @type = type
30
+ # end
31
+ #
32
+ # def positive?
33
+ # type == 'income'
34
+ # end
35
+ # end
36
+ #
37
+ # class Account
38
+ # private
39
+ #
40
+ # def filter_income(transactions)
41
+ # transactions.select(&:positive?)
42
+ # end
43
+ # end
44
+ #
45
+ # hash = {
46
+ # transactions: [
47
+ # { value: 1000.53, type: 'income' },
48
+ # { value: 324.56, type: 'outcome' },
49
+ # { value: 50.23, type: 'income' },
50
+ # { value: 150.00, type: 'outcome' },
51
+ # { value: 10.23, type: 'outcome' },
52
+ # { value: 100.12, type: 'outcome' },
53
+ # { value: 101.00, type: 'outcome' }
54
+ # ]
55
+ # }
56
+ # instance = Account.new
57
+ # fetcher = Arstotzka::Fetcher.new(hash, instance,
58
+ # path: 'transactions',
59
+ # clazz: Transaction,
60
+ # after: :filter_income
61
+ # )
62
+ #
63
+ # fetcher.fetch # retruns [
64
+ # # Transaction.new(value: 1000.53, type: 'income'),
65
+ # # Transaction.new(value: 50.23, type: 'income')
66
+ # # ]
67
+ def fetch
68
+ value = crawler.value(hash)
69
+ value.flatten! if flatten && value.respond_to?(:flatten!)
70
+ value = instance.send(after, value) if after
71
+ value
72
+ end
15
73
 
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
74
+ private
22
75
 
23
- private
76
+ attr_reader :path, :hash, :instance
24
77
 
25
- def crawler
26
- @crawler ||= buidl_crawler
27
- end
78
+ delegate :after, :flatten, to: :options_object
79
+ delegate :wrap, to: :wrapper
28
80
 
29
- def buidl_crawler
30
- Arstotzka::Crawler.new(crawler_options) do |value|
31
- wrap(value)
81
+ def crawler
82
+ @crawler ||= buidl_crawler
32
83
  end
33
- end
34
84
 
35
- def crawler_options
36
- options.slice(:case_type, :compact, :default).merge(path: path)
37
- end
85
+ def buidl_crawler
86
+ Arstotzka::Crawler.new(crawler_options) do |value|
87
+ wrap(value)
88
+ end
89
+ end
38
90
 
39
- def wrapper
40
- @wrapper ||= build_wrapper
41
- end
91
+ def crawler_options
92
+ options.slice(:case_type, :compact, :default).merge(path: path)
93
+ end
42
94
 
43
- def build_wrapper
44
- Arstotzka::Wrapper.new(wrapper_options)
45
- end
95
+ def wrapper
96
+ @wrapper ||= build_wrapper
97
+ end
46
98
 
47
- def wrapper_options
48
- options.slice(:clazz, :type)
99
+ def build_wrapper
100
+ Arstotzka::Wrapper.new(wrapper_options)
101
+ end
102
+
103
+ def wrapper_options
104
+ options.slice(:clazz, :type)
105
+ end
49
106
  end
50
107
  end
@@ -1,33 +1,81 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Arstotzka
4
+ # Reads a value from a hash using the path as list of keys
2
5
  class Reader
3
- attr_reader :path, :case_type
4
-
6
+ # @param path [Array] path of keys broken down as array
7
+ # @param case_type [Symbol] Case of the keys
8
+ # - lower_camel: keys in the hash are lowerCamelCase
9
+ # - upper_camel: keys in the hash are UpperCamelCase
10
+ # - snake: keys in the hash are snake_case
5
11
  def initialize(path:, case_type:)
6
12
  @case_type = case_type
7
- @path = path.map(&self.method(:change_case))
13
+ @path = path.map(&method(:change_case))
8
14
  end
9
15
 
10
- def read(json, index)
16
+ # Reads the value of one key in the hash
17
+ #
18
+ # @param hash [Hash] hash to be read
19
+ # @param index [Integer] Index of the key (in path) to be used
20
+ #
21
+ # @return Object The value fetched from the hash
22
+ #
23
+ # @example
24
+ # hash = {
25
+ # full_name: 'John',
26
+ # 'Age' => 23,
27
+ # 'carCollection' => [
28
+ # { maker: 'Ford', 'model' => 'Model A' },
29
+ # { maker: 'BMW', 'model' => 'Jetta' }
30
+ # ]
31
+ # }
32
+ #
33
+ # reader = Arstotzka::Reader.new(%w(person full_name), case_type: :snake)
34
+ # reader.read(hash, 1) # returns 'John'
35
+ #
36
+ # @example
37
+ # reader = Arstotzka::Reader.new(%w(person age), case_type: :upper_camel)
38
+ # reader.read(hash, 1) # returns 23
39
+ #
40
+ # @example
41
+ # reader = Arstotzka::Reader.new(%w(person car_collection model), case_type: :snake)
42
+ # reader.read(hash, 1) # raises {Arstotzka::Exception::KeyNotFound}
43
+ #
44
+ # @example
45
+ # reader = Arstotzka::Reader.new(%w(person car_collection model), case_type: :lower_camel)
46
+ # reader.read(hash, 1) # returns [
47
+ # # { maker: 'Ford', 'model' => 'Model A' },
48
+ # # { maker: 'BMW', 'model' => 'Jetta' }
49
+ # # ]
50
+ def read(hash, index)
11
51
  key = path[index]
12
52
 
13
- check_key!(json, key)
53
+ check_key!(hash, key)
14
54
 
15
- json.key?(key) ? json[key] : json[key.to_sym]
55
+ hash.key?(key) ? hash[key] : hash[key.to_sym]
16
56
  end
17
57
 
18
- def is_ended?(index)
58
+ # Checks if index is within path range
59
+ #
60
+ # @example
61
+ # reader = Arstotzka::Reader.new(%w(person full_name), case_type: :snake)
62
+ # reader.read(hash, 1) # returns false
63
+ # reader.read(hash, 2) # returns true
64
+ def ended?(index)
19
65
  index >= path.size
20
66
  end
21
67
 
22
68
  private
23
69
 
24
- def check_key!(json, key)
25
- return if has_key?(json, key)
70
+ attr_reader :path, :case_type
71
+
72
+ def check_key!(hash, key)
73
+ return if key?(hash, key)
26
74
  raise Exception::KeyNotFound
27
75
  end
28
76
 
29
- def has_key?(json, key)
30
- json&.key?(key) || json&.key?(key.to_sym)
77
+ def key?(hash, key)
78
+ hash&.key?(key) || hash&.key?(key.to_sym)
31
79
  end
32
80
 
33
81
  def change_case(key)
@@ -1,15 +1,29 @@
1
- module Arstotzka::TypeCast
2
- extend ActiveSupport::Concern
1
+ # frozen_string_literal: true
3
2
 
4
- def to_integer(value)
5
- value.to_i if value.present?
6
- end
3
+ module Arstotzka
4
+ # Concern with all the type cast methods to be used by {Wrapper}
5
+ #
6
+ # Usage of typecast is defined by the configuration of {Builder} by the usage of
7
+ # option type
8
+ module TypeCast
9
+ extend ActiveSupport::Concern
7
10
 
8
- def to_string(value)
9
- value.to_s
10
- end
11
+ # converts a value to integer
12
+ # @return [Integer]
13
+ def to_integer(value)
14
+ value.to_i if value.present?
15
+ end
16
+
17
+ # converts value to string
18
+ # @return [String]
19
+ def to_string(value)
20
+ value.to_s
21
+ end
11
22
 
12
- def to_float(value)
13
- value.to_f if value.present?
23
+ # converts value to float
24
+ # @return [Float]
25
+ def to_float(value)
26
+ value.to_f if value.present?
27
+ end
14
28
  end
15
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Arstotzka
2
- VERSION = '1.0.1'
4
+ VERSION = '1.0.2'
3
5
  end
@@ -1,36 +1,61 @@
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)
1
+ # frozen_string_literal: true
2
+
3
+ module Arstotzka
4
+ # Class responsible for wrapping / parsing a value fetched
5
+ class Wrapper
6
+ include Arstotzka::TypeCast
7
+
8
+ # @param clazz [Class] class to wrap the value
9
+ # @param type [String/Symbol] type to cast the value. The
10
+ # possible type_cast is defined by {TypeCast}
11
+ def initialize(clazz: nil, type: nil)
12
+ @clazz = clazz
13
+ @type = type
14
+ end
15
+
16
+ # wrap a value
17
+ #
18
+ # @example
19
+ # class Person
20
+ # attr_reader :name
21
+ #
22
+ # def initialize(name)
23
+ # @name = name
24
+ # end
25
+ # end
26
+ #
27
+ # wrapper = Arstotzka::Wrapper.new(clazz: Person)
28
+ # wrapper.wrap('John') # retruns Person.new('John')
29
+ #
30
+ # @example
31
+ # wrapper = Arstotzka::Wrapper.new(type: :integer)
32
+ # wrapper.wrap(['10', '20', '30']) # retruns [10, 20, 30]
33
+ def wrap(value)
34
+ return wrap_array(value) if value.is_a?(Array)
35
+ wrap_element(value)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :clazz, :type
41
+
42
+ def wrap_element(value)
43
+ value = cast(value) if type? && !value.nil?
44
+ return if value.nil?
45
+
46
+ clazz ? clazz.new(value) : value
47
+ end
48
+
49
+ def wrap_array(array)
50
+ array.map { |v| wrap v }
51
+ end
52
+
53
+ def type?
54
+ type.present? && type != :none
55
+ end
56
+
57
+ def cast(value)
58
+ public_send("to_#{type}", value)
59
+ end
35
60
  end
36
61
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe 'default option' do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe MyParser do
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Arstotzka::Builder do
6
+ describe 'yard' do
7
+ let!(:instance) { klass.new(hash) }
8
+ let(:hash) do
9
+ {
10
+ 'name' => { first: 'John', last: 'Williams' },
11
+ :age => '20',
12
+ 'cars' => 2.0
13
+ }
14
+ end
15
+
16
+ describe '#first_name' do
17
+ let(:klass) { Class.new(MyModel) }
18
+ let(:builder) { described_class.new([:first_name], klass, full_path: 'name.first') }
19
+
20
+ before do
21
+ builder.build
22
+ end
23
+
24
+ it 'crawls into the hash to find the value of the first name' do
25
+ expect(instance.first_name).to eq('John')
26
+ end
27
+ end
28
+
29
+ describe '#age' do
30
+ let(:klass) { Class.new(MyModel) }
31
+ let(:builder) { described_class.new([:age, 'cars'], klass, type: :integer) }
32
+
33
+ before do
34
+ builder.build
35
+ end
36
+
37
+ it 'crawls into the hash to find the value of the age' do
38
+ expect(instance.age).to eq(20)
39
+ end
40
+
41
+ it do
42
+ expect(instance.age).to be_a(Integer)
43
+ end
44
+ end
45
+
46
+ describe '#cars' do
47
+ let(:klass) { Class.new(MyModel) }
48
+ let(:builder) { described_class.new([:age, 'cars'], klass, type: :integer) }
49
+
50
+ before do
51
+ builder.build
52
+ end
53
+
54
+ it 'crawls into the hash to find the value of the age' do
55
+ expect(instance.cars).to eq(2)
56
+ end
57
+
58
+ it do
59
+ expect(instance.cars).to be_a(Integer)
60
+ end
61
+ end
62
+ end
63
+ end