input_sanitizer 0.2.2 → 0.3.33
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 +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
|