input_sanitizer 0.1.9 → 0.4.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.
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