objective 0.1.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.
- checksums.yaml +7 -0
- data/lib/objective/allow.rb +10 -0
- data/lib/objective/deny.rb +10 -0
- data/lib/objective/discard.rb +10 -0
- data/lib/objective/errors/error_array.rb +21 -0
- data/lib/objective/errors/error_atom.rb +27 -0
- data/lib/objective/errors/error_hash.rb +65 -0
- data/lib/objective/errors/error_message_creator.rb +46 -0
- data/lib/objective/errors/validation_error.rb +14 -0
- data/lib/objective/filter.rb +166 -0
- data/lib/objective/filters/any_filter.rb +7 -0
- data/lib/objective/filters/array_filter.rb +49 -0
- data/lib/objective/filters/boolean_filter.rb +21 -0
- data/lib/objective/filters/date_filter.rb +29 -0
- data/lib/objective/filters/decimal_filter.rb +46 -0
- data/lib/objective/filters/duck_filter.rb +18 -0
- data/lib/objective/filters/file_filter.rb +22 -0
- data/lib/objective/filters/float_filter.rb +45 -0
- data/lib/objective/filters/hash_filter.rb +42 -0
- data/lib/objective/filters/integer_filter.rb +60 -0
- data/lib/objective/filters/model_filter.rb +23 -0
- data/lib/objective/filters/root_filter.rb +54 -0
- data/lib/objective/filters/string_filter.rb +30 -0
- data/lib/objective/filters/time_filter.rb +30 -0
- data/lib/objective/filters.rb +140 -0
- data/lib/objective/none.rb +10 -0
- data/lib/objective/outcome.rb +5 -0
- data/lib/objective/unit.rb +103 -0
- data/lib/objective.rb +53 -0
- data/spec/default_spec.rb +33 -0
- data/spec/errors_spec.rb +135 -0
- data/spec/filters/any_filter_spec.rb +34 -0
- data/spec/filters/array_filter_spec.rb +195 -0
- data/spec/filters/boolean_filter_spec.rb +66 -0
- data/spec/filters/date_filter_spec.rb +145 -0
- data/spec/filters/decimal_filter_spec.rb +199 -0
- data/spec/filters/duck_filter_spec.rb +49 -0
- data/spec/filters/file_filter_spec.rb +93 -0
- data/spec/filters/float_filter_spec.rb +140 -0
- data/spec/filters/hash_filter_spec.rb +65 -0
- data/spec/filters/integer_filter_spec.rb +150 -0
- data/spec/filters/model_filter_spec.rb +98 -0
- data/spec/filters/root_filter_spec.rb +113 -0
- data/spec/filters/string_filter_spec.rb +251 -0
- data/spec/filters/time_filter_spec.rb +123 -0
- data/spec/inheritance_spec.rb +36 -0
- data/spec/simple_unit.rb +18 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/unit_spec.rb +244 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 07f72b29c73d8dc130b6aad5d85b508d3cb3e574
|
4
|
+
data.tar.gz: '09adcd4a2daa8733a07dfd011cdb9aedeb0d5f41'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 549dc86b4ae3352c525da82fb706a5f939d3e27a719638a54356439adf4e3fdbb8d4bb6708cfcfee1e42edfab614dcf0b09007bcc695ecab96c64fe619a2baab
|
7
|
+
data.tar.gz: e7fe8fe0504f224627566395b1aaad7ed17301d7a540b5017463850889cc8396e478ef1bf25b04cadac9f3eb23f7cd53734373bcc4e038ca1a30bde5ac9e05ca
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Errors
|
4
|
+
class ErrorArray < Array
|
5
|
+
def codes
|
6
|
+
map { |e| e&.codes }
|
7
|
+
end
|
8
|
+
|
9
|
+
def message(parent_key = nil, _index = nil)
|
10
|
+
each_with_index.map { |e, i| e&.message(parent_key, i) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def message_list(parent_key = nil, _index = nil)
|
14
|
+
each_with_index.map do |e, i|
|
15
|
+
next if e.nil?
|
16
|
+
e.message_list(parent_key, i)
|
17
|
+
end.flatten.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Errors
|
4
|
+
class ErrorAtom
|
5
|
+
attr_reader :key, :codes, :type, :datum, :bound
|
6
|
+
|
7
|
+
# ErrorAtom.new(:name, :too_short)
|
8
|
+
# ErrorAtom.new(:name, :too_short, message: "is too short")
|
9
|
+
def initialize(key, codes, options = {})
|
10
|
+
@key = key # attribute
|
11
|
+
@codes = codes
|
12
|
+
@message = options[:message]
|
13
|
+
@type = options[:type] # target class/filter of coercion
|
14
|
+
@value = options[:value] # value given
|
15
|
+
@bound = options[:bound] # value of validator
|
16
|
+
end
|
17
|
+
|
18
|
+
def message(parent_key = nil, index = nil)
|
19
|
+
@message ||= Objective.error_message_creator.message(self, parent_key, index)
|
20
|
+
end
|
21
|
+
|
22
|
+
def message_list(parent_key = nil, index = nil)
|
23
|
+
Array.wrap(message(parent_key, index))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Errors
|
4
|
+
class ErrorHash < HashWithIndifferentAccess
|
5
|
+
# objective.errors is an ErrorHash instance like this:
|
6
|
+
# {
|
7
|
+
# email: ErrorAtom(:matches),
|
8
|
+
# name: ErrorAtom(:too_weird, message: "is too weird"),
|
9
|
+
# adddress: { # Nested ErrorHash object
|
10
|
+
# city: ErrorAtom(:not_found, message: "That's not a city, silly!"),
|
11
|
+
# state: ErrorAtom(:in)
|
12
|
+
# }
|
13
|
+
# }
|
14
|
+
|
15
|
+
# Returns a nested HashWithIndifferentAccess where the values are symbols. Eg:
|
16
|
+
# {
|
17
|
+
# email: :matches,
|
18
|
+
# name: :too_weird,
|
19
|
+
# adddress: {
|
20
|
+
# city: :not_found,
|
21
|
+
# state: :in
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
def codes
|
25
|
+
HashWithIndifferentAccess.new.tap do |hash|
|
26
|
+
each do |k, v|
|
27
|
+
hash[k] = v.codes
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a nested HashWithIndifferentAccess where the values are messages. Eg:
|
33
|
+
# {
|
34
|
+
# email: "isn't in the right format",
|
35
|
+
# name: "is too weird",
|
36
|
+
# adddress: {
|
37
|
+
# city: "is not a city",
|
38
|
+
# state: "isn't a valid option"
|
39
|
+
# }
|
40
|
+
# }
|
41
|
+
def message(_parent_key = nil, _index = nil)
|
42
|
+
HashWithIndifferentAccess.new.tap do |hash|
|
43
|
+
each do |k, v|
|
44
|
+
hash[k] = v.message(k)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a flat array where each element is a full sentence. Eg:
|
50
|
+
# [
|
51
|
+
# "Email isn't in the right format.",
|
52
|
+
# "Name is too weird",
|
53
|
+
# "That's not a city, silly!",
|
54
|
+
# "State isn't a valid option."
|
55
|
+
# ]
|
56
|
+
def message_list(_parent_key = nil, _index = nil)
|
57
|
+
list = []
|
58
|
+
each do |k, v|
|
59
|
+
list.concat(v.message_list(k))
|
60
|
+
end
|
61
|
+
list
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Errors
|
4
|
+
class ErrorMessageCreator
|
5
|
+
MESSAGES = Hash.new('is invalid').tap do |h|
|
6
|
+
h.merge!(
|
7
|
+
nils: 'cannot be nil',
|
8
|
+
required: 'is required',
|
9
|
+
|
10
|
+
string: 'must be a string',
|
11
|
+
integer: 'must be an integer',
|
12
|
+
decimal: 'must be a number',
|
13
|
+
boolean: 'must be a boolean',
|
14
|
+
hash: 'must be a hash',
|
15
|
+
array: 'must be an array',
|
16
|
+
model: 'must be the right class',
|
17
|
+
date: 'date does non exist',
|
18
|
+
|
19
|
+
before: 'must be before given date',
|
20
|
+
after: 'must be after given date',
|
21
|
+
empty: 'cannot be empty',
|
22
|
+
matches: 'has an incorrect format',
|
23
|
+
in: 'is not an available option',
|
24
|
+
min: 'is too small',
|
25
|
+
max: 'is too big',
|
26
|
+
|
27
|
+
new_records: 'model must be saved'
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def message(atom, parent_key, index)
|
32
|
+
[
|
33
|
+
index_ordinal(index),
|
34
|
+
(atom.key || parent_key || 'item').to_s.titleize,
|
35
|
+
MESSAGES[atom.codes]
|
36
|
+
]
|
37
|
+
.compact
|
38
|
+
.join(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
def index_ordinal(index)
|
42
|
+
index&.+(1)&.ordinalize
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
class Filter
|
4
|
+
def self.inherited(child_class)
|
5
|
+
method_name = filter_name(child_class)
|
6
|
+
|
7
|
+
define_method(method_name) do |*args, &block|
|
8
|
+
args.unshift(nil) if args[0].is_a?(Hash)
|
9
|
+
new_filter = child_class.new(*args, &block)
|
10
|
+
sub_filters.push(new_filter)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.filter_name(klass = self)
|
15
|
+
filter_name = klass.name.match(/\AObjective::Filters::(.*)Filter\z/)&.[](1)&.underscore
|
16
|
+
raise 'filename error in filters folder' unless filter_name
|
17
|
+
filter_name
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :key
|
21
|
+
attr_reader :sub_filters
|
22
|
+
|
23
|
+
def initialize(key = nil, opts = {}, &block)
|
24
|
+
@key = key
|
25
|
+
@given_options = opts
|
26
|
+
@sub_filters ||= []
|
27
|
+
|
28
|
+
instance_eval(&block) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
def options
|
32
|
+
@options ||= OpenStruct.new(type_specific_options_hash.merge(@given_options))
|
33
|
+
end
|
34
|
+
|
35
|
+
def type_specific_options_hash
|
36
|
+
Objective::Filters::Config[self.class.filter_name].to_h
|
37
|
+
end
|
38
|
+
|
39
|
+
def sub_filters_hash
|
40
|
+
sub_filters.each_with_object({}) { |sf, result| result[sf.key] = sf }
|
41
|
+
end
|
42
|
+
|
43
|
+
def dup
|
44
|
+
dupped = self.class.new
|
45
|
+
sub_filters.each { |sf| dupped.sub_filters.push(sf) }
|
46
|
+
dupped
|
47
|
+
end
|
48
|
+
|
49
|
+
def default?
|
50
|
+
options.to_h.key?(:default)
|
51
|
+
end
|
52
|
+
|
53
|
+
def default
|
54
|
+
options.default
|
55
|
+
end
|
56
|
+
|
57
|
+
def feed(raw)
|
58
|
+
return feed_none if raw == Objective::NONE
|
59
|
+
return feed_nil if raw.nil?
|
60
|
+
|
61
|
+
coerced = options.strict == true ? raw : coerce(raw)
|
62
|
+
errors = coerce_error(coerced)
|
63
|
+
return feed_invalid(errors, raw, raw) if errors
|
64
|
+
|
65
|
+
errors = validate(coerced)
|
66
|
+
return feed_empty(raw, coerced) if errors == :empty
|
67
|
+
|
68
|
+
feed_result(errors, raw, coerced)
|
69
|
+
end
|
70
|
+
|
71
|
+
def feed_none
|
72
|
+
case options.none
|
73
|
+
when Objective::ALLOW
|
74
|
+
return Objective::DISCARD
|
75
|
+
when Objective::DENY
|
76
|
+
coerced = Objective::NONE
|
77
|
+
errors = :required
|
78
|
+
when Objective::DISCARD
|
79
|
+
raise 'the none option cannot be discarded — did you mean to use allow instead?'
|
80
|
+
else
|
81
|
+
coerced = options.none
|
82
|
+
end
|
83
|
+
|
84
|
+
feed_result(errors, Objective::NONE, coerced)
|
85
|
+
end
|
86
|
+
|
87
|
+
def feed_nil
|
88
|
+
case options.nils
|
89
|
+
when Objective::ALLOW
|
90
|
+
coerced = nil
|
91
|
+
errors = nil
|
92
|
+
when Objective::DENY
|
93
|
+
coerced = nil
|
94
|
+
errors = :nils
|
95
|
+
when Objective::DISCARD
|
96
|
+
return Objective::DISCARD
|
97
|
+
else
|
98
|
+
coerced = options.nils
|
99
|
+
end
|
100
|
+
|
101
|
+
feed_result(errors, nil, coerced)
|
102
|
+
end
|
103
|
+
|
104
|
+
def feed_invalid(errors, raw, coerced)
|
105
|
+
case options.invalid
|
106
|
+
when Objective::DENY
|
107
|
+
# nothing
|
108
|
+
when Objective::DISCARD
|
109
|
+
return Objective::DISCARD
|
110
|
+
else
|
111
|
+
coerced = options.invalid
|
112
|
+
end
|
113
|
+
|
114
|
+
feed_result(errors, raw, coerced)
|
115
|
+
end
|
116
|
+
|
117
|
+
def feed_empty(raw, coerced)
|
118
|
+
case options.empty
|
119
|
+
when Objective::ALLOW
|
120
|
+
errors = nil
|
121
|
+
when Objective::DENY
|
122
|
+
errors = :empty
|
123
|
+
when Objective::DISCARD
|
124
|
+
return Objective::DISCARD
|
125
|
+
else
|
126
|
+
coerced = options.empty
|
127
|
+
end
|
128
|
+
|
129
|
+
feed_result(errors, raw, coerced)
|
130
|
+
end
|
131
|
+
|
132
|
+
def feed_result(errors, raw, coerced)
|
133
|
+
OpenStruct.new(
|
134
|
+
errors: errors,
|
135
|
+
raw: raw,
|
136
|
+
coerced: coerced,
|
137
|
+
inputs: coerced
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
def coerce(raw)
|
142
|
+
raw
|
143
|
+
end
|
144
|
+
|
145
|
+
def coerce_error(coerced)
|
146
|
+
end
|
147
|
+
|
148
|
+
def validate(coerced)
|
149
|
+
end
|
150
|
+
|
151
|
+
def handle_errors(given_error)
|
152
|
+
return if given_error.nil?
|
153
|
+
|
154
|
+
case given_error
|
155
|
+
when Objective::Errors::ErrorHash
|
156
|
+
given_error
|
157
|
+
when Objective::Errors::ErrorArray
|
158
|
+
given_error
|
159
|
+
when Objective::Errors::ErrorAtom
|
160
|
+
given_error
|
161
|
+
else
|
162
|
+
Objective::Errors::ErrorAtom.new(key, given_error)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class ArrayFilter < Objective::Filter
|
5
|
+
def feed(raw)
|
6
|
+
result = super(raw)
|
7
|
+
return result if result == Objective::DISCARD || result.errors || result.inputs.nil?
|
8
|
+
|
9
|
+
inputs = []
|
10
|
+
errors = Objective::Errors::ErrorArray.new
|
11
|
+
|
12
|
+
sub_filter = sub_filters.first
|
13
|
+
|
14
|
+
index_shift = 0
|
15
|
+
|
16
|
+
data = result.coerced
|
17
|
+
data.each_with_index do |sub_data, index|
|
18
|
+
sub_result = sub_filter.feed(sub_data)
|
19
|
+
if sub_result == Objective::DISCARD
|
20
|
+
index_shift += 1
|
21
|
+
next
|
22
|
+
end
|
23
|
+
|
24
|
+
sub_data = sub_result.inputs
|
25
|
+
sub_error = sub_result.errors
|
26
|
+
|
27
|
+
unless sub_error.nil?
|
28
|
+
errors[index - index_shift] = sub_filter.handle_errors(sub_error)
|
29
|
+
end
|
30
|
+
|
31
|
+
inputs[index - index_shift] = sub_data
|
32
|
+
end
|
33
|
+
|
34
|
+
result.inputs = inputs
|
35
|
+
result.errors = errors.present? ? errors : nil
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def coerce(raw)
|
40
|
+
return Array.wrap(raw) if options.wrap
|
41
|
+
raw
|
42
|
+
end
|
43
|
+
|
44
|
+
def coerce_error(coerced)
|
45
|
+
return :array unless coerced.is_a?(Array)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class BooleanFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
def coerce(raw)
|
8
|
+
return raw if boolean?(raw)
|
9
|
+
options.coercion_map[raw.to_s.downcase] if raw.respond_to?(:to_s)
|
10
|
+
end
|
11
|
+
|
12
|
+
def coerce_error(coerced)
|
13
|
+
return :boolean unless boolean?(coerced)
|
14
|
+
end
|
15
|
+
|
16
|
+
def boolean?(datum)
|
17
|
+
datum == true || datum == false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class DateFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
def coerce(raw)
|
8
|
+
return raw if raw.is_a?(Date) # Date and DateTime
|
9
|
+
return raw.to_date if raw.respond_to?(:to_date)
|
10
|
+
parse(raw) if raw.is_a?(String)
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
options.format ? Date.strptime(data, options.format) : Date.parse(data)
|
15
|
+
rescue ArgumentError
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce_error(coerced)
|
20
|
+
return :date unless coerced.is_a?(Date)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate(coerced)
|
24
|
+
return :after if options.after && coerced <= options.after
|
25
|
+
return :before if options.before && coerced >= options.before
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class DecimalFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
# TODO: the Rational class should be coerced - it requires a precision argument
|
8
|
+
def coerce(datum)
|
9
|
+
return datum if datum.blank?
|
10
|
+
|
11
|
+
return datum.to_d if datum.is_a?(Integer) || datum.is_a?(Float)
|
12
|
+
|
13
|
+
return datum unless datum.is_a?(String)
|
14
|
+
|
15
|
+
clean_str = datum.tr(options.delimiter, '').tr(options.decimal_mark, '.')
|
16
|
+
return datum unless clean_str =~ /\A[-+]?\d*\.?\d*\z/
|
17
|
+
clean_str.to_d
|
18
|
+
end
|
19
|
+
|
20
|
+
def coerce_error(coerced)
|
21
|
+
return :decimal unless coerced.is_a?(BigDecimal)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate(datum)
|
25
|
+
return :min unless above_min?(datum)
|
26
|
+
return :max unless below_max?(datum)
|
27
|
+
return :scale unless within_scale?(datum)
|
28
|
+
end
|
29
|
+
|
30
|
+
def above_min?(datum)
|
31
|
+
return true if options.min.nil?
|
32
|
+
datum >= options.min
|
33
|
+
end
|
34
|
+
|
35
|
+
def below_max?(datum)
|
36
|
+
return true if options.max.nil?
|
37
|
+
datum <= options.max
|
38
|
+
end
|
39
|
+
|
40
|
+
def within_scale?(datum)
|
41
|
+
return true if options.scale.nil?
|
42
|
+
(datum - datum.round(options.scale)).zero?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class DuckFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
def coerce_error(coerced)
|
8
|
+
return :duck unless respond_to_all?(coerced)
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to_all?(coerced)
|
12
|
+
Array.wrap(options[:methods]).map do |method|
|
13
|
+
coerced.respond_to?(method)
|
14
|
+
end.all?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class FileFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
def coerce_error(coerced)
|
8
|
+
return :file unless respond_to_all?(coerced)
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to_all?(coerced)
|
12
|
+
methods = %i(read size)
|
13
|
+
methods.concat(%i(original_filename content_type)) if options.upload
|
14
|
+
methods.map { |method| coerced.respond_to?(method) }.all?
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(coerced)
|
18
|
+
return :size if options.size && coerced.size > options.size
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class FloatFilter < Objective::Filter
|
5
|
+
private
|
6
|
+
|
7
|
+
def coerce(datum)
|
8
|
+
return datum if datum.blank?
|
9
|
+
|
10
|
+
return datum.to_f if datum.is_a?(Integer) || datum.is_a?(BigDecimal)
|
11
|
+
|
12
|
+
return datum unless datum.is_a?(String)
|
13
|
+
|
14
|
+
clean_str = datum.tr(options.delimiter, '').tr(options.decimal_mark, '.')
|
15
|
+
return datum unless clean_str =~ /\A[-+]?\d*\.?\d*\z/
|
16
|
+
clean_str.to_f
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce_error(coerced)
|
20
|
+
return :float unless coerced.is_a?(Float)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate(coerced)
|
24
|
+
return :min unless above_min?(coerced)
|
25
|
+
return :max unless below_max?(coerced)
|
26
|
+
return :scale unless within_scale?(coerced)
|
27
|
+
end
|
28
|
+
|
29
|
+
def above_min?(datum)
|
30
|
+
return true if options.min.nil?
|
31
|
+
datum >= options.min
|
32
|
+
end
|
33
|
+
|
34
|
+
def below_max?(datum)
|
35
|
+
return true if options.max.nil?
|
36
|
+
datum <= options.max
|
37
|
+
end
|
38
|
+
|
39
|
+
def within_scale?(datum)
|
40
|
+
return true if options.scale.nil?
|
41
|
+
(datum - datum.round(options.scale)).zero?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Objective
|
3
|
+
module Filters
|
4
|
+
class HashFilter < Objective::Filter
|
5
|
+
def feed(raw)
|
6
|
+
result = super(raw)
|
7
|
+
return result if result == Objective::DISCARD || result.errors || result.inputs.nil?
|
8
|
+
|
9
|
+
errors = Objective::Errors::ErrorHash.new
|
10
|
+
inputs = HashWithIndifferentAccess.new
|
11
|
+
|
12
|
+
data = result.coerced
|
13
|
+
sub_filters_hash.each_pair do |key, key_filter|
|
14
|
+
datum = data.key?(key) ? data[key] : Objective::NONE
|
15
|
+
key_filter_result = key_filter.feed(datum)
|
16
|
+
next if key_filter_result == Objective::DISCARD
|
17
|
+
|
18
|
+
sub_data = key_filter_result.inputs
|
19
|
+
sub_error = key_filter_result.errors
|
20
|
+
|
21
|
+
if sub_error.nil?
|
22
|
+
inputs[key] = sub_data
|
23
|
+
else
|
24
|
+
errors[key] = key_filter.handle_errors(sub_error)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
result.inputs = inputs
|
29
|
+
result.errors = errors.present? ? errors : nil
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def coerce(raw)
|
34
|
+
raw.try(:with_indifferent_access)
|
35
|
+
end
|
36
|
+
|
37
|
+
def coerce_error(coerced)
|
38
|
+
return :hash unless coerced.is_a?(Hash)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|