arstotzka 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +11 -0
- data/.rubocop_todo.yml +20 -0
- data/Gemfile +3 -2
- data/Guardfile +3 -2
- data/README.md +24 -5
- data/Rakefile +5 -3
- data/arstotzka.gemspec +18 -15
- data/arstotzka.jpg +0 -0
- data/lib/arstotzka.rb +2 -0
- data/lib/arstotzka/builder.rb +109 -47
- data/lib/arstotzka/class_methods.rb +34 -0
- data/lib/arstotzka/crawler.rb +86 -11
- data/lib/arstotzka/exception.rb +7 -2
- data/lib/arstotzka/fetcher.rb +93 -36
- data/lib/arstotzka/reader.rb +59 -11
- data/lib/arstotzka/type_cast.rb +24 -10
- data/lib/arstotzka/version.rb +3 -1
- data/lib/arstotzka/wrapper.rb +59 -34
- data/spec/integration/readme/default_spec.rb +2 -0
- data/spec/integration/readme/my_parser_spec.rb +2 -0
- data/spec/integration/yard/arstotzka/builder_spec.rb +63 -0
- data/spec/integration/yard/arstotzka/class_methods_spec.rb +49 -0
- data/spec/integration/yard/arstotzka/crawler_spec.rb +77 -0
- data/spec/integration/yard/arstotzka/fetcher_spec.rb +51 -0
- data/spec/integration/yard/arstotzka/reader_spec.rb +77 -0
- data/spec/integration/yard/arstotzka/wrapper_spec.rb +29 -0
- data/spec/lib/arstotzka/builder_spec.rb +6 -4
- data/spec/lib/arstotzka/crawler_spec.rb +30 -17
- data/spec/lib/arstotzka/fetcher_spec.rb +4 -2
- data/spec/lib/arstotzka/reader_spec.rb +13 -11
- data/spec/lib/arstotzka/wrapper_spec.rb +10 -10
- data/spec/lib/arstotzka_spec.rb +12 -8
- data/spec/spec_helper.rb +6 -2
- data/spec/support/fixture_helpers.rb +4 -2
- data/spec/support/models.rb +4 -3
- data/spec/support/models/account.rb +9 -0
- data/spec/support/models/arstotzka/dummy.rb +21 -17
- data/spec/support/models/arstotzka/fetcher/dummy.rb +7 -2
- data/spec/support/models/arstotzka/type_cast.rb +7 -4
- data/spec/support/models/arstotzka/wrapper/dummy.rb +10 -5
- data/spec/support/models/game.rb +2 -0
- data/spec/support/models/house.rb +2 -0
- data/spec/support/models/my_model.rb +9 -0
- data/spec/support/models/my_parser.rb +6 -5
- data/spec/support/models/person.rb +7 -1
- data/spec/support/models/star.rb +2 -0
- data/spec/support/models/star_gazer.rb +3 -2
- data/spec/support/models/transaction.rb +19 -0
- data/spec/support/shared_examples/wrapper.rb +6 -5
- metadata +64 -15
data/lib/arstotzka/exception.rb
CHANGED
data/lib/arstotzka/fetcher.rb
CHANGED
@@ -1,50 +1,107 @@
|
|
1
|
-
|
2
|
-
include Sinclair::OptionsParser
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
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
|
-
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
76
|
+
attr_reader :path, :hash, :instance
|
24
77
|
|
25
|
-
|
26
|
-
|
27
|
-
end
|
78
|
+
delegate :after, :flatten, to: :options_object
|
79
|
+
delegate :wrap, to: :wrapper
|
28
80
|
|
29
|
-
|
30
|
-
|
31
|
-
wrap(value)
|
81
|
+
def crawler
|
82
|
+
@crawler ||= buidl_crawler
|
32
83
|
end
|
33
|
-
end
|
34
84
|
|
35
|
-
|
36
|
-
|
37
|
-
|
85
|
+
def buidl_crawler
|
86
|
+
Arstotzka::Crawler.new(crawler_options) do |value|
|
87
|
+
wrap(value)
|
88
|
+
end
|
89
|
+
end
|
38
90
|
|
39
|
-
|
40
|
-
|
41
|
-
|
91
|
+
def crawler_options
|
92
|
+
options.slice(:case_type, :compact, :default).merge(path: path)
|
93
|
+
end
|
42
94
|
|
43
|
-
|
44
|
-
|
45
|
-
|
95
|
+
def wrapper
|
96
|
+
@wrapper ||= build_wrapper
|
97
|
+
end
|
46
98
|
|
47
|
-
|
48
|
-
|
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
|
data/lib/arstotzka/reader.rb
CHANGED
@@ -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
|
-
|
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(&
|
13
|
+
@path = path.map(&method(:change_case))
|
8
14
|
end
|
9
15
|
|
10
|
-
|
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!(
|
53
|
+
check_key!(hash, key)
|
14
54
|
|
15
|
-
|
55
|
+
hash.key?(key) ? hash[key] : hash[key.to_sym]
|
16
56
|
end
|
17
57
|
|
18
|
-
|
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
|
-
|
25
|
-
|
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
|
30
|
-
|
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)
|
data/lib/arstotzka/type_cast.rb
CHANGED
@@ -1,15 +1,29 @@
|
|
1
|
-
|
2
|
-
extend ActiveSupport::Concern
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
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
|
data/lib/arstotzka/version.rb
CHANGED
data/lib/arstotzka/wrapper.rb
CHANGED
@@ -1,36 +1,61 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
@@ -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
|