input_sanitizer 0.2.2 → 0.3.33

Sign up to get free protection for your applications and to get access to all the features.
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