input_sanitizer 0.2.2 → 0.3.33
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +2 -0
- data/CHANGELOG +92 -0
- data/LICENSE +201 -22
- data/README.md +7 -0
- data/input_sanitizer.gemspec +15 -5
- data/lib/input_sanitizer/errors.rb +142 -0
- data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
- data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
- data/lib/input_sanitizer/extended_converters.rb +5 -55
- data/lib/input_sanitizer/restricted_hash.rb +49 -8
- data/lib/input_sanitizer/v1/clean_field.rb +38 -0
- data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +8 -11
- data/lib/input_sanitizer/v1/sanitizer.rb +163 -0
- data/lib/input_sanitizer/v1.rb +22 -0
- data/lib/input_sanitizer/v2/clean_field.rb +36 -0
- data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
- data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
- data/lib/input_sanitizer/v2/error_collection.rb +49 -0
- data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
- data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
- data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
- data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
- data/lib/input_sanitizer/v2/types.rb +213 -0
- data/lib/input_sanitizer/v2.rb +13 -0
- data/lib/input_sanitizer/version.rb +1 -1
- data/lib/input_sanitizer.rb +5 -2
- data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
- data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
- data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
- data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
- data/spec/restricted_hash_spec.rb +37 -7
- data/spec/sanitizer_spec.rb +32 -22
- data/spec/spec_helper.rb +3 -1
- data/spec/{default_converters_spec.rb → v1/default_converters_spec.rb} +27 -9
- data/spec/v2/converters_spec.rb +174 -0
- data/spec/v2/payload_sanitizer_spec.rb +460 -0
- data/spec/v2/payload_transform_spec.rb +98 -0
- data/spec/v2/query_sanitizer_spec.rb +300 -0
- data/v2.md +52 -0
- metadata +86 -30
- data/Gemfile.lock +0 -44
- data/lib/input_sanitizer/sanitizer.rb +0 -179
- data/spec/extended_converters_spec.rb +0 -78
@@ -0,0 +1,22 @@
|
|
1
|
+
module InputSanitizer::V1
|
2
|
+
end
|
3
|
+
|
4
|
+
dir = File.dirname(__FILE__)
|
5
|
+
require File.join(dir, 'errors')
|
6
|
+
require File.join(dir, 'restricted_hash')
|
7
|
+
require File.join(dir, 'v1', 'default_converters')
|
8
|
+
require File.join(dir, 'v1', 'clean_field')
|
9
|
+
require File.join(dir, 'v1', 'sanitizer')
|
10
|
+
require File.join(dir, 'extended_converters')
|
11
|
+
|
12
|
+
# Backward compatibility
|
13
|
+
module InputSanitizer
|
14
|
+
Sanitizer = V1::Sanitizer
|
15
|
+
|
16
|
+
IntegerConverter = V1::IntegerConverter
|
17
|
+
StringConverter = V1::StringConverter
|
18
|
+
DateConverter = V1::DateConverter
|
19
|
+
TimeConverter = V1::TimeConverter
|
20
|
+
BooleanConverter = V1::BooleanConverter
|
21
|
+
AllowNil = V1::AllowNil
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class InputSanitizer::V2::CleanField < MethodStruct.new(:data, :has_key, :default, :collection, :type, :converter, :options)
|
2
|
+
def call
|
3
|
+
if has_key
|
4
|
+
convert
|
5
|
+
elsif default
|
6
|
+
converter.call(default, options)
|
7
|
+
elsif options[:required]
|
8
|
+
raise InputSanitizer::ValueMissingError
|
9
|
+
else
|
10
|
+
raise InputSanitizer::OptionalValueOmitted
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def convert
|
16
|
+
if collection
|
17
|
+
collection_clean.call(
|
18
|
+
:data => data,
|
19
|
+
:collection => collection,
|
20
|
+
:converter => converter,
|
21
|
+
:options => options
|
22
|
+
)
|
23
|
+
else
|
24
|
+
converter.call(data, options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def collection_clean
|
29
|
+
case type
|
30
|
+
when :payload
|
31
|
+
InputSanitizer::V2::CleanPayloadCollectionField
|
32
|
+
when :query
|
33
|
+
InputSanitizer::V2::CleanQueryCollectionField
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class InputSanitizer::V2::CleanPayloadCollectionField < MethodStruct.new(:data, :converter, :collection, :options)
|
2
|
+
def call
|
3
|
+
return nil if options[:allow_nil] && data == nil
|
4
|
+
|
5
|
+
validate_type
|
6
|
+
validate_size
|
7
|
+
|
8
|
+
result, errors = [], {}
|
9
|
+
|
10
|
+
data.each_with_index do |value, idx|
|
11
|
+
begin
|
12
|
+
result << converter.call(value, options)
|
13
|
+
rescue InputSanitizer::ValidationError => e
|
14
|
+
errors[idx] = e
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
if errors.any?
|
19
|
+
raise InputSanitizer::CollectionError.new(errors)
|
20
|
+
else
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def validate_type
|
27
|
+
unless data.respond_to?(:to_ary)
|
28
|
+
raise InputSanitizer::TypeMismatchError.new(data, :array)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_size
|
33
|
+
if collection.respond_to?(:fetch)
|
34
|
+
if collection[:minimum] && data.length < collection[:minimum]
|
35
|
+
raise InputSanitizer::CollectionLengthError.new(data.length, collection[:minimum], collection[:maximum])
|
36
|
+
elsif collection[:maximum] && data.length > collection[:maximum]
|
37
|
+
raise InputSanitizer::CollectionLengthError.new(data.length, collection[:minimum], collection[:maximum])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class InputSanitizer::V2::CleanQueryCollectionField < MethodStruct.new(:data, :converter, :collection, :options)
|
2
|
+
def call
|
3
|
+
validate_type
|
4
|
+
validate_size
|
5
|
+
|
6
|
+
result, errors = [], {}
|
7
|
+
items.each_with_index do |value, idx|
|
8
|
+
begin
|
9
|
+
result << converter.call(value, options)
|
10
|
+
rescue InputSanitizer::ValidationError => e
|
11
|
+
errors[idx] = e
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if errors.any?
|
16
|
+
raise InputSanitizer::CollectionError.new(errors)
|
17
|
+
else
|
18
|
+
result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def items
|
24
|
+
@items ||= data.to_s.split(',')
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_type
|
28
|
+
InputSanitizer::V2::Types::StringCheck.new.call(data)
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_size
|
32
|
+
if collection.respond_to?(:fetch)
|
33
|
+
if collection[:minimum] && items.length < collection[:minimum]
|
34
|
+
raise InputSanitizer::CollectionLengthError.new(items.length, collection[:minimum], collection[:maximum])
|
35
|
+
elsif collection[:maximum] && items.length > collection[:maximum]
|
36
|
+
raise InputSanitizer::CollectionLengthError.new(items.length, collection[:minimum], collection[:maximum])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class InputSanitizer::V2::ErrorCollection
|
2
|
+
include Enumerable
|
3
|
+
|
4
|
+
attr_reader :error_codes, :messages
|
5
|
+
|
6
|
+
def initialize(errors)
|
7
|
+
@error_codes = {}
|
8
|
+
@messages = {}
|
9
|
+
@error_details = {}
|
10
|
+
|
11
|
+
errors.each do |error|
|
12
|
+
(@error_codes[error.field] ||= []) << error.code
|
13
|
+
(@messages[error.field] ||= []) << error.message
|
14
|
+
error_value_hash = { :value => error.value }
|
15
|
+
(@error_details[error.field] ||= []) << error_value_hash
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](attribute)
|
20
|
+
@error_codes[attribute]
|
21
|
+
end
|
22
|
+
|
23
|
+
def each
|
24
|
+
@error_codes.each_key do |attribute|
|
25
|
+
self[attribute].each { |error| yield attribute, error }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
@error_codes.size
|
31
|
+
end
|
32
|
+
alias_method :length, :size
|
33
|
+
alias_method :count, :size
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
@error_codes.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(attribute, code = :invalid, options = {})
|
40
|
+
(@error_codes[attribute] ||= []) << code
|
41
|
+
(@messages[attribute] ||= []) << options.delete(:messages)
|
42
|
+
(@error_details[attribute] ||= []) << options
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
messages.dup
|
47
|
+
end
|
48
|
+
alias_method :full_messages, :to_hash
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class InputSanitizer::V2::NestedSanitizerFactory
|
2
|
+
class NilAllowed
|
3
|
+
def cleaned
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def valid?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.for(nested_sanitizer_klass, value, options)
|
13
|
+
if value.nil? && options[:allow_nil] && !options[:collection]
|
14
|
+
NilAllowed.new
|
15
|
+
else
|
16
|
+
nested_sanitizer_klass.new(value, options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
class InputSanitizer::V2::PayloadSanitizer < InputSanitizer::Sanitizer
|
2
|
+
attr_reader :validation_context
|
3
|
+
|
4
|
+
def initialize(data, validation_context = {})
|
5
|
+
super data
|
6
|
+
|
7
|
+
self.validation_context = validation_context || {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def validation_context=(context)
|
11
|
+
raise ArgumentError, "validation_context must be a Hash" unless context && context.is_a?(Hash)
|
12
|
+
@validation_context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_collection
|
16
|
+
@error_collection ||= InputSanitizer::V2::ErrorCollection.new(errors)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.converters
|
20
|
+
{
|
21
|
+
:integer => InputSanitizer::V2::Types::IntegerCheck.new,
|
22
|
+
:float => InputSanitizer::V2::Types::FloatCheck.new,
|
23
|
+
:string => InputSanitizer::V2::Types::StringCheck.new,
|
24
|
+
:boolean => InputSanitizer::V2::Types::BooleanCheck.new,
|
25
|
+
:datetime => InputSanitizer::V2::Types::DatetimeCheck.new,
|
26
|
+
:date => InputSanitizer::V2::Types::DatetimeCheck.new(:check_date => true),
|
27
|
+
:url => InputSanitizer::V2::Types::URLCheck.new,
|
28
|
+
}
|
29
|
+
end
|
30
|
+
initialize_types_dsl
|
31
|
+
|
32
|
+
def self.nested(*keys)
|
33
|
+
options = keys.pop
|
34
|
+
sanitizer = options.delete(:sanitizer)
|
35
|
+
keys.push(options)
|
36
|
+
raise "You did not define a sanitizer for nested value" if sanitizer == nil
|
37
|
+
converter = lambda { |value, converter_options|
|
38
|
+
instance = InputSanitizer::V2::NestedSanitizerFactory.for(sanitizer, value, converter_options)
|
39
|
+
raise InputSanitizer::NestedError.new(instance.errors) unless instance.valid?
|
40
|
+
instance.cleaned
|
41
|
+
}
|
42
|
+
|
43
|
+
keys << {} unless keys.last.is_a?(Hash)
|
44
|
+
keys.last[:nested] = true
|
45
|
+
|
46
|
+
self.set_keys_to_converter(keys, converter)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def perform_clean
|
51
|
+
super
|
52
|
+
@data.reject { |key, _| self.class.fields.keys.include?(key) }.each { |key, _| @errors << InputSanitizer::ExtraneousParamError.new("/#{key}") }
|
53
|
+
end
|
54
|
+
|
55
|
+
def prepare_options!(options)
|
56
|
+
return options if @validation_context.empty?
|
57
|
+
context = @validation_context.dup
|
58
|
+
context_provided_values = context.delete(:provided)
|
59
|
+
|
60
|
+
intersection = options.keys & context.keys
|
61
|
+
|
62
|
+
unless intersection.empty?
|
63
|
+
message = "validation context and converter options have the same keys: #{intersection}. " \
|
64
|
+
"In order to proceed please fix the configuration. " \
|
65
|
+
"In the meantime aborting ..."
|
66
|
+
raise RuntimeError, message
|
67
|
+
end
|
68
|
+
|
69
|
+
if context_provided_values
|
70
|
+
options[:provided] ||= {}
|
71
|
+
options[:provided] = options[:provided].merge(context_provided_values)
|
72
|
+
end
|
73
|
+
|
74
|
+
options.merge(context)
|
75
|
+
end
|
76
|
+
|
77
|
+
def clean_field(field, hash)
|
78
|
+
options = hash[:options].clone
|
79
|
+
collection = options.delete(:collection)
|
80
|
+
default = options.delete(:default)
|
81
|
+
has_key = @data.has_key?(field)
|
82
|
+
value = @data[field]
|
83
|
+
is_nested = options.delete(:nested)
|
84
|
+
|
85
|
+
provide = options.delete(:provide)
|
86
|
+
provided = Array(provide).inject({}) { |memo, value| memo[value] = @data[value]; memo }
|
87
|
+
options[:provided] = provided
|
88
|
+
|
89
|
+
if is_nested && has_key
|
90
|
+
if collection
|
91
|
+
raise InputSanitizer::TypeMismatchError.new(value, "array") unless value.is_a?(Array)
|
92
|
+
elsif !options[:allow_nil] || (options[:allow_nil] && !value.nil?)
|
93
|
+
raise InputSanitizer::TypeMismatchError.new(value, "hash") unless value.is_a?(Hash)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
@cleaned[field] = InputSanitizer::V2::CleanField.call(
|
98
|
+
:data => value,
|
99
|
+
:has_key => has_key,
|
100
|
+
:default => default,
|
101
|
+
:collection => collection,
|
102
|
+
:type => sanitizer_type,
|
103
|
+
:converter => hash[:converter],
|
104
|
+
:options => prepare_options!(options)
|
105
|
+
)
|
106
|
+
rescue InputSanitizer::OptionalValueOmitted
|
107
|
+
rescue InputSanitizer::ValidationError => error
|
108
|
+
@errors += handle_error(field, error)
|
109
|
+
end
|
110
|
+
|
111
|
+
def handle_error(field, error)
|
112
|
+
case error
|
113
|
+
when InputSanitizer::CollectionError
|
114
|
+
error.collection_errors.map do |index, error|
|
115
|
+
handle_error("#{field}/#{index}", error)
|
116
|
+
end
|
117
|
+
when InputSanitizer::NestedError
|
118
|
+
error.nested_errors.map do |error|
|
119
|
+
handle_error("#{field}#{error.field}", error)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
error.field = "/#{field}"
|
123
|
+
Array(error)
|
124
|
+
end.flatten
|
125
|
+
end
|
126
|
+
|
127
|
+
def sanitizer_type
|
128
|
+
:payload
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
|
3
|
+
class InputSanitizer::V2::PayloadTransform
|
4
|
+
attr_reader :original_payload, :context
|
5
|
+
|
6
|
+
def self.call(original_payload, context = {})
|
7
|
+
new(original_payload, context).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(original_payload, context = {})
|
11
|
+
fail "#{self.class} is missing #transform method" unless respond_to?(:transform)
|
12
|
+
@original_payload, @context = original_payload, context
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
transform
|
17
|
+
payload
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def rename(from, to)
|
22
|
+
if has?(from)
|
23
|
+
data = payload.delete(from)
|
24
|
+
payload[to] = data
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge_in(field, options = {})
|
29
|
+
if source = payload.delete(field)
|
30
|
+
source = options[:using].call(source) if options[:using]
|
31
|
+
payload.merge!(source)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def has?(key)
|
36
|
+
payload.has_key?(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def payload
|
40
|
+
@payload ||= original_payload.with_indifferent_access
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class InputSanitizer::V2::QuerySanitizer < InputSanitizer::V2::PayloadSanitizer
|
2
|
+
def self.converters
|
3
|
+
{
|
4
|
+
:integer => InputSanitizer::V2::Types::CoercingIntegerCheck.new,
|
5
|
+
:float => InputSanitizer::V2::Types::CoercingFloatCheck.new,
|
6
|
+
:string => InputSanitizer::V2::Types::StringCheck.new,
|
7
|
+
:boolean => InputSanitizer::V2::Types::CoercingBooleanCheck.new,
|
8
|
+
:datetime => InputSanitizer::V2::Types::DatetimeCheck.new,
|
9
|
+
:date => InputSanitizer::V2::Types::DatetimeCheck.new(:check_date => true),
|
10
|
+
:url => InputSanitizer::V2::Types::URLCheck.new,
|
11
|
+
}
|
12
|
+
end
|
13
|
+
initialize_types_dsl
|
14
|
+
|
15
|
+
def self.sort_by(allowed_values, options = {})
|
16
|
+
set_keys_to_converter([:sort_by, { :allow => allowed_values }.merge(options)], InputSanitizer::V2::Types::SortByCheck.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
# allow underscore cache buster by default
|
20
|
+
string :_
|
21
|
+
|
22
|
+
private
|
23
|
+
def perform_clean
|
24
|
+
super
|
25
|
+
@errors.each do |error|
|
26
|
+
error.field = error.field[1..-1] if error.field.start_with?('/')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def sanitizer_type
|
31
|
+
:query
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module InputSanitizer::V2::Types
|
4
|
+
class IntegerCheck
|
5
|
+
def call(value, options = {})
|
6
|
+
if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
|
7
|
+
raise InputSanitizer::BlankValueError
|
8
|
+
elsif value == nil
|
9
|
+
value
|
10
|
+
else
|
11
|
+
Integer(value).tap do |integer|
|
12
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer) unless integer == value
|
13
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
|
14
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
rescue ArgumentError, TypeError
|
18
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class CoercingIntegerCheck
|
23
|
+
def call(value, options = {})
|
24
|
+
if value == nil || value == 'null'
|
25
|
+
if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
|
26
|
+
raise InputSanitizer::BlankValueError
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
else
|
31
|
+
Integer(value).tap do |integer|
|
32
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
|
33
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
rescue ArgumentError
|
37
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class FloatCheck
|
42
|
+
def call(value, options = {})
|
43
|
+
if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
|
44
|
+
raise InputSanitizer::BlankValueError
|
45
|
+
elsif value == nil
|
46
|
+
value
|
47
|
+
else
|
48
|
+
Float(value).tap do |float|
|
49
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float) unless float == value
|
50
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
|
51
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue ArgumentError, TypeError
|
55
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class CoercingFloatCheck
|
60
|
+
def call(value, options = {})
|
61
|
+
if value == nil || value == 'null'
|
62
|
+
if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
|
63
|
+
raise InputSanitizer::BlankValueError
|
64
|
+
else
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
else
|
68
|
+
Float(value).tap do |float|
|
69
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
|
70
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue ArgumentError
|
74
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class StringCheck
|
79
|
+
def call(value, options = {})
|
80
|
+
if options[:allow] && !options[:allow].include?(value)
|
81
|
+
raise InputSanitizer::ValueNotAllowedError.new(value)
|
82
|
+
elsif value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
83
|
+
raise InputSanitizer::BlankValueError
|
84
|
+
elsif options[:regexp] && options[:regexp].match(value).nil?
|
85
|
+
raise InputSanitizer::RegexpMismatchError.new
|
86
|
+
elsif value == nil && options[:allow_nil] == false
|
87
|
+
raise InputSanitizer::BlankValueError
|
88
|
+
elsif value.blank?
|
89
|
+
value
|
90
|
+
else
|
91
|
+
value.to_s.tap do |string|
|
92
|
+
raise InputSanitizer::TypeMismatchError.new(value, :string) unless string == value
|
93
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && string.length < options[:minimum]
|
94
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && string.length > options[:maximum]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class BooleanCheck
|
101
|
+
def call(value, options = {})
|
102
|
+
if value == nil
|
103
|
+
raise InputSanitizer::BlankValueError
|
104
|
+
elsif [true, false].include?(value)
|
105
|
+
value
|
106
|
+
else
|
107
|
+
raise InputSanitizer::TypeMismatchError.new(value, :boolean)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class CoercingBooleanCheck
|
113
|
+
def call(value, options = {})
|
114
|
+
if [true, 'true'].include?(value)
|
115
|
+
true
|
116
|
+
elsif [false, 'false'].include?(value)
|
117
|
+
false
|
118
|
+
else
|
119
|
+
raise InputSanitizer::TypeMismatchError.new(value, :boolean)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class DatetimeCheck
|
125
|
+
def initialize(options = {})
|
126
|
+
@check_date = options && options[:check_date]
|
127
|
+
@klass = @check_date ? Date : DateTime
|
128
|
+
end
|
129
|
+
|
130
|
+
def call(value, options = {})
|
131
|
+
raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime) unless value == nil || value.is_a?(String)
|
132
|
+
|
133
|
+
if value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
134
|
+
raise InputSanitizer::BlankValueError
|
135
|
+
elsif value == nil && options[:allow_nil] == false
|
136
|
+
raise InputSanitizer::BlankValueError
|
137
|
+
elsif value.blank?
|
138
|
+
value
|
139
|
+
else
|
140
|
+
@klass.parse(value).tap do |datetime|
|
141
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && datetime < options[:minimum]
|
142
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && datetime > options[:maximum]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
rescue ArgumentError, TypeError
|
146
|
+
raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class URLCheck
|
151
|
+
def call(value, options = {})
|
152
|
+
if value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
153
|
+
raise InputSanitizer::BlankValueError
|
154
|
+
elsif value == nil && options[:allow_nil] == false
|
155
|
+
raise InputSanitizer::BlankValueError
|
156
|
+
elsif value.blank?
|
157
|
+
value
|
158
|
+
else
|
159
|
+
unless /\A#{URI.regexp(%w(http https)).to_s}\z/.match(value)
|
160
|
+
raise InputSanitizer::TypeMismatchError.new(value, :url)
|
161
|
+
end
|
162
|
+
value
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class SortByCheck
|
168
|
+
def call(value, options = {})
|
169
|
+
check_options!(options)
|
170
|
+
|
171
|
+
key, direction = split(value)
|
172
|
+
direction = 'asc' if direction.blank?
|
173
|
+
|
174
|
+
# special case when fallback takes care of separator sanitization e.g. custom fields
|
175
|
+
if options[:fallback] && !allowed_directions.include?(direction)
|
176
|
+
direction = 'asc'
|
177
|
+
key = value
|
178
|
+
end
|
179
|
+
|
180
|
+
unless valid?(key, direction, options)
|
181
|
+
raise InputSanitizer::ValueNotAllowedError.new(value)
|
182
|
+
end
|
183
|
+
|
184
|
+
[key, direction]
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
def valid?(key, direction, options)
|
189
|
+
allowed_keys = options[:allow]
|
190
|
+
fallback = options[:fallback]
|
191
|
+
|
192
|
+
allowed_directions.include?(direction) &&
|
193
|
+
((allowed_keys && allowed_keys.include?(key)) ||
|
194
|
+
(fallback && fallback.call(key, direction, options)))
|
195
|
+
end
|
196
|
+
|
197
|
+
def split(value)
|
198
|
+
head, _, tail = value.to_s.rpartition(':')
|
199
|
+
head.empty? ? [tail, head] : [head, tail]
|
200
|
+
end
|
201
|
+
|
202
|
+
def check_options!(options)
|
203
|
+
fallback = options[:fallback]
|
204
|
+
if fallback && !fallback.respond_to?(:call)
|
205
|
+
raise ArgumentError, ":fallback option must respond to method :call (proc, lambda etc)"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def allowed_directions
|
210
|
+
['asc', 'desc']
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|