input_sanitizer 0.1.9 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gempush.yml +28 -0
  3. data/.gitignore +2 -1
  4. data/.travis.yml +4 -8
  5. data/CHANGELOG +96 -0
  6. data/LICENSE +201 -22
  7. data/README.md +22 -3
  8. data/input_sanitizer.gemspec +10 -4
  9. data/lib/input_sanitizer.rb +5 -2
  10. data/lib/input_sanitizer/errors.rb +142 -0
  11. data/lib/input_sanitizer/extended_converters.rb +5 -52
  12. data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
  13. data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
  14. data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
  15. data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
  16. data/lib/input_sanitizer/restricted_hash.rb +49 -8
  17. data/lib/input_sanitizer/v1.rb +22 -0
  18. data/lib/input_sanitizer/v1/clean_field.rb +38 -0
  19. data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +30 -13
  20. data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
  21. data/lib/input_sanitizer/v2.rb +13 -0
  22. data/lib/input_sanitizer/v2/clean_field.rb +36 -0
  23. data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
  24. data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
  25. data/lib/input_sanitizer/v2/error_collection.rb +49 -0
  26. data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
  27. data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
  28. data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
  29. data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
  30. data/lib/input_sanitizer/v2/types.rb +213 -0
  31. data/lib/input_sanitizer/version.rb +1 -1
  32. data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
  33. data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
  34. data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
  35. data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
  36. data/spec/restricted_hash_spec.rb +37 -7
  37. data/spec/sanitizer_spec.rb +129 -26
  38. data/spec/spec_helper.rb +17 -2
  39. data/spec/v1/default_converters_spec.rb +141 -0
  40. data/spec/v2/converters_spec.rb +174 -0
  41. data/spec/v2/payload_sanitizer_spec.rb +460 -0
  42. data/spec/v2/payload_transform_spec.rb +98 -0
  43. data/spec/v2/query_sanitizer_spec.rb +300 -0
  44. data/v2.md +52 -0
  45. metadata +105 -40
  46. data/lib/input_sanitizer/sanitizer.rb +0 -152
  47. data/spec/default_converters_spec.rb +0 -101
  48. data/spec/extended_converters_spec.rb +0 -62
@@ -1,5 +1,8 @@
1
1
  module InputSanitizer
2
2
  end
3
3
 
4
- require "input_sanitizer/version"
5
- require 'input_sanitizer/sanitizer'
4
+ require 'method_struct'
5
+
6
+ require 'input_sanitizer/version'
7
+ require 'input_sanitizer/v1'
8
+ require 'input_sanitizer/v2'
@@ -0,0 +1,142 @@
1
+ module InputSanitizer
2
+ class OptionalValueOmitted < StandardError; end
3
+
4
+ class KeyNotAllowedError < ArgumentError; end
5
+
6
+ class ValidationError < StandardError
7
+ attr_accessor :field
8
+ attr_reader :value
9
+ end
10
+
11
+ class ConversionError < ValidationError; end
12
+
13
+ class ValueMissingError < ValidationError
14
+ def code
15
+ :missing
16
+ end
17
+
18
+ def initialize
19
+ super("is missing")
20
+ end
21
+ end
22
+
23
+ class BlankValueError < ValidationError
24
+ def code
25
+ :blank
26
+ end
27
+
28
+ def initialize
29
+ super("can't be blank")
30
+ end
31
+ end
32
+
33
+ class RegexpMismatchError < ValidationError
34
+ def code
35
+ :regexp
36
+ end
37
+
38
+ def initialize
39
+ super("does not match regular expression")
40
+ end
41
+ end
42
+
43
+ class ValueNotAllowedError < ValidationError
44
+ def code
45
+ :inclusion
46
+ end
47
+
48
+ def initialize(value)
49
+ @value = value
50
+ super("is not included in the list")
51
+ end
52
+ end
53
+
54
+ class TypeMismatchError < ValidationError
55
+ def code
56
+ case @type
57
+ when :integer
58
+ :not_an_integer
59
+ when :url
60
+ :invalid_uri
61
+ else
62
+ :invalid_type
63
+ end
64
+ end
65
+
66
+ def initialize(value, type)
67
+ @value = value
68
+ @type = type
69
+
70
+ message = case @type
71
+ when :integer
72
+ "must be an integer"
73
+ when :url
74
+ 'must be a valid URI (include the scheme name part, both http and https are accepted, '\
75
+ 'and the hierarchical part)'
76
+ else
77
+ "must be a value of type '#{type}'"
78
+ end
79
+
80
+ super(message)
81
+ end
82
+ end
83
+
84
+ class ExtraneousParamError < ValidationError
85
+ def code
86
+ :unknown_param
87
+ end
88
+
89
+ def initialize(name)
90
+ @field = name
91
+ super("is an unexpected parameter")
92
+ end
93
+ end
94
+
95
+ class CollectionLengthError < ValidationError
96
+ def code
97
+ :invalid_length
98
+ end
99
+
100
+ def initialize(value, min, max)
101
+ if min && max
102
+ super("must be of length between #{min} and #{max}, given: #{value}")
103
+ elsif min
104
+ super("must be of length greater than or equal to #{min}, given: #{value}")
105
+ else
106
+ super("must be of length less than or equal to #{max}, given: #{value}")
107
+ end
108
+ end
109
+ end
110
+
111
+ class ValueError < ValidationError
112
+ def code
113
+ :invalid_value
114
+ end
115
+
116
+ def initialize(value, min, max)
117
+ if min && max
118
+ super("must be between #{min} and #{max}, given: #{value}")
119
+ elsif min
120
+ super("must be higher than or equal to #{min}, given: #{value}")
121
+ else
122
+ super("must be lower than or equal to #{max}, given: #{value}")
123
+ end
124
+ end
125
+ end
126
+
127
+ class NestedError < ValidationError
128
+ attr_reader :nested_errors
129
+
130
+ def initialize(nested_errors)
131
+ @nested_errors = nested_errors
132
+ end
133
+ end
134
+
135
+ class CollectionError < ValidationError
136
+ attr_reader :collection_errors
137
+
138
+ def initialize(collection_errors)
139
+ @collection_errors = collection_errors
140
+ end
141
+ end
142
+ end
@@ -1,52 +1,5 @@
1
- module InputSanitizer
2
- module AllowNil
3
- def call(value)
4
- if value.nil? || value == ""
5
- nil
6
- else
7
- super(value)
8
- end
9
- end
10
- end
11
-
12
- class PositiveIntegerConverter < IntegerConverter
13
- def call(value)
14
- val = super
15
- raise ConversionError.new("invalid integer (neagtive or zero)") if val <= 0
16
- val
17
- end
18
- end
19
-
20
- class CommaJoinedIntegersConverter
21
- def call(value)
22
- non_valid = value.gsub(/[0-9,]/, "")
23
- if non_valid.empty?
24
- parts = value.split(",").map(&:to_i)
25
- else
26
- invalid_chars = non_valid.split(//)
27
- invalid_chars_desc = invalid_chars.join(", ")
28
- raise InputSanitizer::ConversionError.new("Invalid chars: #{invalid_chars_desc}")
29
- end
30
- end
31
- end
32
-
33
- class SpecificValuesConverter
34
- def initialize(values)
35
- @valid_values = values
36
- end
37
-
38
- def call(value)
39
- found = @valid_values.include?(value) ? value : nil
40
- if !found
41
- found = @valid_values.include?(value.to_sym) ? value.to_sym : nil
42
- end
43
- if !found
44
- values_joined = @valid_values.join(", ")
45
- error_message = "Possible values: #{values_joined}"
46
- raise InputSanitizer::ConversionError.new(error_message)
47
- else
48
- found
49
- end
50
- end
51
- end
52
- 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')
@@ -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,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() { |hash, key| default_for_key(key) }
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
- private
15
- def default_for_key(key)
16
- key_allowed?(key) ? nil : raise_not_allowed(key)
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.new(msg)
62
+ raise KeyNotAllowedError, msg
22
63
  end
23
64
  end
24
65
  end
@@ -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,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,14 +1,12 @@
1
1
  require 'time'
2
+ require 'date'
2
3
 
3
- module InputSanitizer
4
- class ConversionError < Exception
5
- end
6
-
4
+ module InputSanitizer::V1
7
5
  class IntegerConverter
8
6
  def call(value)
9
7
  cast = value.to_i
10
8
  if cast.to_s != value.to_s
11
- raise ConversionError.new("invalid integer")
9
+ raise InputSanitizer::ConversionError.new("invalid integer")
12
10
  end
13
11
  cast
14
12
  end
@@ -24,24 +22,31 @@ module InputSanitizer
24
22
  ISO_RE = /\A\d{4}-?\d{2}-?\d{2}/
25
23
 
26
24
  def call(value)
27
- raise ConversionError.new("invalid time") unless value =~ ISO_RE
25
+ raise InputSanitizer::ConversionError.new("invalid time") unless value =~ ISO_RE
28
26
  Date.parse(value)
29
27
  rescue ArgumentError
30
- raise ConversionError.new("invalid iso8601 date")
28
+ raise InputSanitizer::ConversionError.new("invalid iso8601 date")
31
29
  end
32
30
  end
33
31
 
34
32
  class TimeConverter
35
- ISO_RE = /\A\d{4}-?\d{2}-?\d{2}([T ]?\d{2}(:?\d{2}(:?\d{2})?)?)?\Z/
33
+ ISO_RE = /\A\d{4}-?\d{2}-?\d{2}([T ]?\d{2}(:?\d{2}(:?\d{2}((\.)?\d{0,3}(Z)?)?)?)?)?\Z/
36
34
 
37
35
  def call(value)
38
- if value =~ ISO_RE
39
- strip_timezone(Time.parse(value))
36
+ case value
37
+ when Time
38
+ value.getutc
39
+ when String
40
+ if value =~ ISO_RE
41
+ strip_timezone(Time.parse(value))
42
+ else
43
+ raise InputSanitizer::ConversionError.new("invalid time")
44
+ end
40
45
  else
41
- raise ConversionError.new("invalid time")
46
+ raise InputSanitizer::ConversionError.new("invalid time")
42
47
  end
43
48
  rescue ArgumentError
44
- raise ConversionError.new("invalid time")
49
+ raise InputSanitizer::ConversionError.new("invalid time")
45
50
  end
46
51
 
47
52
  def strip_timezone(time)
@@ -59,6 +64,8 @@ module InputSanitizer
59
64
  '0' => false,
60
65
  'yes' => true,
61
66
  'no' => false,
67
+ 1 => true,
68
+ 0 => false,
62
69
  }
63
70
 
64
71
  def call(value)
@@ -74,7 +81,17 @@ module InputSanitizer
74
81
  message += " for true, or "
75
82
  message += falsy.join(", ")
76
83
  message += " for false."
77
- raise ConversionError.new(message)
84
+ raise InputSanitizer::ConversionError.new(message)
85
+ end
86
+ end
87
+ end
88
+
89
+ module AllowNil
90
+ def call(value)
91
+ if value.nil? || value == ""
92
+ nil
93
+ else
94
+ super(value)
78
95
  end
79
96
  end
80
97
  end