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,15 @@
|
|
1
|
+
module InputSanitizer
|
2
|
+
class CommaJoinedIntegersConverter
|
3
|
+
def call(value)
|
4
|
+
value = value.to_s
|
5
|
+
non_valid = value.gsub(/[0-9,]/, "")
|
6
|
+
|
7
|
+
if non_valid.empty?
|
8
|
+
value.split(",").map(&:to_i)
|
9
|
+
else
|
10
|
+
invalid_chars = non_valid.split(//).join(", ")
|
11
|
+
raise InputSanitizer::ConversionError.new("Invalid integers: #{invalid_chars}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module InputSanitizer
|
2
|
+
class CommaJoinedStringsConverter
|
3
|
+
def call(value)
|
4
|
+
value = value.to_s
|
5
|
+
non_valid = value.gsub(/[a-zA-Z,_]/, "")
|
6
|
+
|
7
|
+
if non_valid.empty?
|
8
|
+
value.split(",").map(&:to_s)
|
9
|
+
else
|
10
|
+
invalid_chars = non_valid.split(//).join(", ")
|
11
|
+
raise InputSanitizer::ConversionError.new("Invalid strings: #{invalid_chars}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module InputSanitizer
|
2
|
+
class PositiveIntegerConverter < V1::IntegerConverter
|
3
|
+
def call(value)
|
4
|
+
super.tap { |value| raise_error if value <= 0 }
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
def raise_error
|
9
|
+
raise ConversionError.new("invalid integer (neagtive or zero)")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module InputSanitizer
|
2
|
+
class SpecificValuesConverter
|
3
|
+
def initialize(values)
|
4
|
+
@valid_values = values
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(value)
|
8
|
+
case
|
9
|
+
when @valid_values.include?(value)
|
10
|
+
value
|
11
|
+
when value.respond_to?(:to_sym) && @valid_values.include?(value.to_sym)
|
12
|
+
value.to_sym
|
13
|
+
else
|
14
|
+
values_joined = @valid_values.join(", ")
|
15
|
+
raise InputSanitizer::ConversionError.new("Possible values: #{values_joined}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,55 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
val
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
class CommaJoinedIntegersConverter
|
11
|
-
def call(value)
|
12
|
-
non_valid = value.gsub(/[0-9,]/, "")
|
13
|
-
if non_valid.empty?
|
14
|
-
parts = value.split(",").map(&:to_i)
|
15
|
-
else
|
16
|
-
invalid_chars = non_valid.split(//)
|
17
|
-
invalid_chars_desc = invalid_chars.join(", ")
|
18
|
-
raise InputSanitizer::ConversionError.new("Invalid chars: #{invalid_chars_desc}")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class CommaJoinedStringsConverter
|
24
|
-
def call(value)
|
25
|
-
non_valid = value.gsub(/[a-zA-Z,]/, "")
|
26
|
-
if non_valid.empty?
|
27
|
-
parts = value.split(",").map(&:to_s)
|
28
|
-
else
|
29
|
-
invalid_chars = non_valid.split(//)
|
30
|
-
invalid_chars_desc = invalid_chars.join(", ")
|
31
|
-
raise InputSanitizer::ConversionError.new("Invalid chars: #{invalid_chars_desc}")
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
class SpecificValuesConverter
|
37
|
-
def initialize(values)
|
38
|
-
@valid_values = values
|
39
|
-
end
|
40
|
-
|
41
|
-
def call(value)
|
42
|
-
found = @valid_values.include?(value) ? value : nil
|
43
|
-
if !found
|
44
|
-
found = @valid_values.include?(value.to_sym) ? value.to_sym : nil
|
45
|
-
end
|
46
|
-
if !found
|
47
|
-
values_joined = @valid_values.join(", ")
|
48
|
-
error_message = "Possible values: #{values_joined}"
|
49
|
-
raise InputSanitizer::ConversionError.new(error_message)
|
50
|
-
else
|
51
|
-
found
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
require File.join(dir, 'extended_converters', 'positive_integer_converter')
|
3
|
+
require File.join(dir, 'extended_converters', 'comma_joined_integers_converter')
|
4
|
+
require File.join(dir, 'extended_converters', 'comma_joined_strings_converter')
|
5
|
+
require File.join(dir, 'extended_converters', 'specific_values_converter')
|
@@ -1,24 +1,65 @@
|
|
1
1
|
module InputSanitizer
|
2
|
-
class KeyNotAllowedError < ArgumentError; end
|
3
|
-
|
4
2
|
class RestrictedHash < Hash
|
5
3
|
def initialize(allowed_keys)
|
6
|
-
@allowed_keys = allowed_keys
|
7
|
-
super()
|
4
|
+
@allowed_keys = Set.new(allowed_keys)
|
5
|
+
super()
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
raise_not_allowed(key) unless key_allowed?(key)
|
10
|
+
fetch(key, nil)
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, val)
|
14
|
+
@allowed_keys.add(key)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def store(key, val)
|
19
|
+
@allowed_keys.add(key)
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def merge!(hash, &block)
|
24
|
+
@allowed_keys.merge(Set[*hash.keys])
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge(hash, &block)
|
29
|
+
@allowed_keys.merge(Set[*hash.keys])
|
30
|
+
super
|
8
31
|
end
|
9
32
|
|
10
33
|
def key_allowed?(key)
|
11
34
|
@allowed_keys.include?(key)
|
12
35
|
end
|
13
36
|
|
14
|
-
|
15
|
-
|
16
|
-
|
37
|
+
def transform_keys
|
38
|
+
return enum_for(:transform_keys) unless block_given?
|
39
|
+
|
40
|
+
new_allowed_keys = @allowed_keys.map { |key| yield(key) }
|
41
|
+
result = self.class.new(new_allowed_keys)
|
42
|
+
each_key do |key|
|
43
|
+
result[yield(key)] = self[key]
|
44
|
+
end
|
45
|
+
result
|
17
46
|
end
|
18
47
|
|
48
|
+
def transform_keys!
|
49
|
+
return enum_for(:transform_keys!) unless block_given?
|
50
|
+
|
51
|
+
@allowed_keys.map! { |key| yield(key) }
|
52
|
+
keys.each do |key|
|
53
|
+
self[yield(key)] = delete(key)
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
19
60
|
def raise_not_allowed(key)
|
20
61
|
msg = "Key not allowed: #{key}"
|
21
|
-
raise KeyNotAllowedError
|
62
|
+
raise KeyNotAllowedError, msg
|
22
63
|
end
|
23
64
|
end
|
24
65
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
InputSanitizer::V1::CleanField = MethodStruct.new(:data, :has_key, :converter, :required, :collection, :namespace, :default, :provide, :allow) do
|
2
|
+
def call
|
3
|
+
if has_key
|
4
|
+
convert
|
5
|
+
elsif default
|
6
|
+
converter.call(default)
|
7
|
+
elsif 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
|
+
data.map { |value| convert_single(value) }
|
18
|
+
else
|
19
|
+
convert_single(data)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def convert_single(value)
|
24
|
+
if namespace
|
25
|
+
{ namespace => convert_value(value[namespace]) }
|
26
|
+
else
|
27
|
+
convert_value(value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_value(value)
|
32
|
+
if provide
|
33
|
+
converter.call(value, provide)
|
34
|
+
else
|
35
|
+
converter.call(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,15 +1,12 @@
|
|
1
1
|
require 'time'
|
2
2
|
require 'date'
|
3
3
|
|
4
|
-
module InputSanitizer
|
5
|
-
class ConversionError < Exception
|
6
|
-
end
|
7
|
-
|
4
|
+
module InputSanitizer::V1
|
8
5
|
class IntegerConverter
|
9
6
|
def call(value)
|
10
7
|
cast = value.to_i
|
11
8
|
if cast.to_s != value.to_s
|
12
|
-
raise ConversionError.new("invalid integer")
|
9
|
+
raise InputSanitizer::ConversionError.new("invalid integer")
|
13
10
|
end
|
14
11
|
cast
|
15
12
|
end
|
@@ -25,10 +22,10 @@ module InputSanitizer
|
|
25
22
|
ISO_RE = /\A\d{4}-?\d{2}-?\d{2}/
|
26
23
|
|
27
24
|
def call(value)
|
28
|
-
raise ConversionError.new("invalid time") unless value =~ ISO_RE
|
25
|
+
raise InputSanitizer::ConversionError.new("invalid time") unless value =~ ISO_RE
|
29
26
|
Date.parse(value)
|
30
27
|
rescue ArgumentError
|
31
|
-
raise ConversionError.new("invalid iso8601 date")
|
28
|
+
raise InputSanitizer::ConversionError.new("invalid iso8601 date")
|
32
29
|
end
|
33
30
|
end
|
34
31
|
|
@@ -43,13 +40,13 @@ module InputSanitizer
|
|
43
40
|
if value =~ ISO_RE
|
44
41
|
strip_timezone(Time.parse(value))
|
45
42
|
else
|
46
|
-
raise ConversionError.new("invalid time")
|
43
|
+
raise InputSanitizer::ConversionError.new("invalid time")
|
47
44
|
end
|
48
45
|
else
|
49
|
-
raise ConversionError.new("invalid time")
|
46
|
+
raise InputSanitizer::ConversionError.new("invalid time")
|
50
47
|
end
|
51
48
|
rescue ArgumentError
|
52
|
-
raise ConversionError.new("invalid time")
|
49
|
+
raise InputSanitizer::ConversionError.new("invalid time")
|
53
50
|
end
|
54
51
|
|
55
52
|
def strip_timezone(time)
|
@@ -84,7 +81,7 @@ module InputSanitizer
|
|
84
81
|
message += " for true, or "
|
85
82
|
message += falsy.join(", ")
|
86
83
|
message += " for false."
|
87
|
-
raise ConversionError.new(message)
|
84
|
+
raise InputSanitizer::ConversionError.new(message)
|
88
85
|
end
|
89
86
|
end
|
90
87
|
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
class InputSanitizer::V1::Sanitizer
|
2
|
+
def initialize(data)
|
3
|
+
@data = symbolize_keys(data)
|
4
|
+
@performed = false
|
5
|
+
@errors = []
|
6
|
+
@cleaned = InputSanitizer::RestrictedHash.new(self.class.fields.keys)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.clean(data)
|
10
|
+
new(data).cleaned
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](field)
|
14
|
+
cleaned[field]
|
15
|
+
end
|
16
|
+
|
17
|
+
def cleaned
|
18
|
+
return @cleaned if @performed
|
19
|
+
|
20
|
+
perform_clean
|
21
|
+
|
22
|
+
@performed = true
|
23
|
+
@cleaned.freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
cleaned
|
28
|
+
@errors.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
cleaned
|
33
|
+
@errors
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.converters
|
37
|
+
{
|
38
|
+
:integer => InputSanitizer::V1::IntegerConverter.new,
|
39
|
+
:string => InputSanitizer::V1::StringConverter.new,
|
40
|
+
:date => InputSanitizer::V1::DateConverter.new,
|
41
|
+
:time => InputSanitizer::V1::TimeConverter.new,
|
42
|
+
:boolean => InputSanitizer::V1::BooleanConverter.new,
|
43
|
+
:integer_or_blank => InputSanitizer::V1::IntegerConverter.new.extend(InputSanitizer::V1::AllowNil),
|
44
|
+
:string_or_blank => InputSanitizer::V1::StringConverter.new.extend(InputSanitizer::V1::AllowNil),
|
45
|
+
:date_or_blank => InputSanitizer::V1::DateConverter.new.extend(InputSanitizer::V1::AllowNil),
|
46
|
+
:time_or_blank => InputSanitizer::V1::TimeConverter.new.extend(InputSanitizer::V1::AllowNil),
|
47
|
+
:boolean_or_blank => InputSanitizer::V1::BooleanConverter.new.extend(InputSanitizer::V1::AllowNil),
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.inherited(subclass)
|
52
|
+
subclass.fields = self.fields.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.initialize_types_dsl
|
56
|
+
converters.keys.each do |name|
|
57
|
+
class_eval <<-END
|
58
|
+
def self.#{name}(*keys)
|
59
|
+
set_keys_to_converter(keys, :#{name})
|
60
|
+
end
|
61
|
+
END
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
initialize_types_dsl
|
66
|
+
|
67
|
+
def self.custom(*keys)
|
68
|
+
options = keys.pop
|
69
|
+
converter = options.delete(:converter)
|
70
|
+
keys.push(options)
|
71
|
+
raise "You did not define a converter for a custom type" if converter == nil
|
72
|
+
self.set_keys_to_converter(keys, converter)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.nested(*keys)
|
76
|
+
options = keys.pop
|
77
|
+
sanitizer = options.delete(:sanitizer)
|
78
|
+
keys.push(options)
|
79
|
+
raise "You did not define a sanitizer for nested value" if sanitizer == nil
|
80
|
+
converter = lambda { |value|
|
81
|
+
instance = sanitizer.new(value)
|
82
|
+
raise InputSanitizer::ConversionError.new(instance.errors) unless instance.valid?
|
83
|
+
instance.cleaned
|
84
|
+
}
|
85
|
+
|
86
|
+
keys << {} unless keys.last.is_a?(Hash)
|
87
|
+
keys.last[:nested] = true
|
88
|
+
|
89
|
+
self.set_keys_to_converter(keys, converter)
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
def self.fields
|
94
|
+
@fields ||= {}
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.fields=(new_fields)
|
98
|
+
@fields = new_fields
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def self.extract_options!(array)
|
103
|
+
array.last.is_a?(Hash) ? array.pop : {}
|
104
|
+
end
|
105
|
+
|
106
|
+
def perform_clean
|
107
|
+
self.class.fields.each { |field, hash| clean_field(field, hash) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def clean_field(field, hash)
|
111
|
+
if hash[:options][:nested] && @data.has_key?(field)
|
112
|
+
if hash[:options][:collection]
|
113
|
+
raise InputSanitizer::ConversionError.new("expected an array") unless @data[field].is_a?(Array)
|
114
|
+
else
|
115
|
+
raise InputSanitizer::ConversionError.new("expected a hash") unless @data[field].is_a?(Hash)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
@cleaned[field] = InputSanitizer::V1::CleanField.call(hash[:options].merge({
|
120
|
+
:has_key => @data.has_key?(field),
|
121
|
+
:data => @data[field],
|
122
|
+
:converter => hash[:converter],
|
123
|
+
:provide => @data[hash[:options][:provide]],
|
124
|
+
}))
|
125
|
+
rescue InputSanitizer::ConversionError => error
|
126
|
+
add_error(field, :invalid_value, @data[field], error.message)
|
127
|
+
rescue InputSanitizer::ValueMissingError => error
|
128
|
+
add_error(field, :missing, nil, nil)
|
129
|
+
rescue InputSanitizer::OptionalValueOmitted
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_error(field, error_type, value, description = nil)
|
133
|
+
@errors << {
|
134
|
+
:field => field,
|
135
|
+
:type => error_type,
|
136
|
+
:value => value,
|
137
|
+
:description => description
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def symbolize_keys(data)
|
142
|
+
data.inject({}) do |memo, kv|
|
143
|
+
memo[kv.first.to_sym] = kv.last
|
144
|
+
memo
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.set_keys_to_converter(keys, converter_or_type)
|
149
|
+
options = extract_options!(keys)
|
150
|
+
converter = if converter_or_type.is_a?(Symbol)
|
151
|
+
converters[converter_or_type]
|
152
|
+
else
|
153
|
+
converter_or_type
|
154
|
+
end
|
155
|
+
|
156
|
+
keys.each do |key|
|
157
|
+
fields[key] = {
|
158
|
+
:converter => converter,
|
159
|
+
:options => options
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|