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.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.travis.yml +2 -0
  4. data/CHANGELOG +92 -0
  5. data/LICENSE +201 -22
  6. data/README.md +7 -0
  7. data/input_sanitizer.gemspec +15 -5
  8. data/lib/input_sanitizer/errors.rb +142 -0
  9. data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
  10. data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
  11. data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
  12. data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
  13. data/lib/input_sanitizer/extended_converters.rb +5 -55
  14. data/lib/input_sanitizer/restricted_hash.rb +49 -8
  15. data/lib/input_sanitizer/v1/clean_field.rb +38 -0
  16. data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +8 -11
  17. data/lib/input_sanitizer/v1/sanitizer.rb +163 -0
  18. data/lib/input_sanitizer/v1.rb +22 -0
  19. data/lib/input_sanitizer/v2/clean_field.rb +36 -0
  20. data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
  21. data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
  22. data/lib/input_sanitizer/v2/error_collection.rb +49 -0
  23. data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
  24. data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
  25. data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
  26. data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
  27. data/lib/input_sanitizer/v2/types.rb +213 -0
  28. data/lib/input_sanitizer/v2.rb +13 -0
  29. data/lib/input_sanitizer/version.rb +1 -1
  30. data/lib/input_sanitizer.rb +5 -2
  31. data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
  32. data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
  33. data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
  34. data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
  35. data/spec/restricted_hash_spec.rb +37 -7
  36. data/spec/sanitizer_spec.rb +32 -22
  37. data/spec/spec_helper.rb +3 -1
  38. data/spec/{default_converters_spec.rb → v1/default_converters_spec.rb} +27 -9
  39. data/spec/v2/converters_spec.rb +174 -0
  40. data/spec/v2/payload_sanitizer_spec.rb +460 -0
  41. data/spec/v2/payload_transform_spec.rb +98 -0
  42. data/spec/v2/query_sanitizer_spec.rb +300 -0
  43. data/v2.md +52 -0
  44. metadata +86 -30
  45. data/Gemfile.lock +0 -44
  46. data/lib/input_sanitizer/sanitizer.rb +0 -179
  47. 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
- module InputSanitizer
2
- class PositiveIntegerConverter < IntegerConverter
3
- def call(value)
4
- val = super
5
- raise ConversionError.new("invalid integer (neagtive or zero)") if val <= 0
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() { |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,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