input_sanitizer 0.1.8
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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +70 -0
- data/Rakefile +2 -0
- data/input_sanitizer.gemspec +21 -0
- data/lib/input_sanitizer/default_converters.rb +81 -0
- data/lib/input_sanitizer/extended_converters.rb +42 -0
- data/lib/input_sanitizer/restricted_hash.rb +24 -0
- data/lib/input_sanitizer/sanitizer.rb +147 -0
- data/lib/input_sanitizer/version.rb +3 -0
- data/lib/input_sanitizer.rb +5 -0
- data/spec/default_converters_spec.rb +101 -0
- data/spec/extended_converters_spec.rb +43 -0
- data/spec/restricted_hash_spec.rb +19 -0
- data/spec/sanitizer_spec.rb +237 -0
- data/spec/spec_helper.rb +7 -0
- metadata +106 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Tomek Paczkowski, Tomasz Werbicki, Michal Bugno
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# InputSanitizer [](http://travis-ci.org/futuresimple/input_sanitizer)
|
2
|
+
|
3
|
+
Gem to sanitize hash of incoming data
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'input_sanitizer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install input_sanitizer
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
|
23
|
+
class PersonSanitizer < InputSanitizer::Sanitizer
|
24
|
+
string :name
|
25
|
+
string :address
|
26
|
+
integer :height
|
27
|
+
float :weight
|
28
|
+
date :birthday
|
29
|
+
end
|
30
|
+
|
31
|
+
# filters unwanted parameters
|
32
|
+
sanitizer = PersonSanitizer.new({:account_id => 1, :name => "John"})
|
33
|
+
sanitizer.cleaned() # => {:name => "John"}
|
34
|
+
|
35
|
+
# provides key access
|
36
|
+
sanitizer[:name] # => "John"
|
37
|
+
|
38
|
+
# also provides shortcut method, same as new({}).cleaned
|
39
|
+
PersonSanitizer.clean({:account_id => 1})
|
40
|
+
|
41
|
+
# supports inheritance
|
42
|
+
class PrivilegedSanitizer < PersonSanitizer
|
43
|
+
integer :account_id
|
44
|
+
end
|
45
|
+
|
46
|
+
PrivilegedSanitizer.clean({:account_id => 1})
|
47
|
+
# => {:account_id => 1}
|
48
|
+
|
49
|
+
# handles type conversions
|
50
|
+
PrivilegedSanitizer.clean({:account_id => '1'})
|
51
|
+
# => {:account_id => 1}
|
52
|
+
|
53
|
+
PrivilegedSanitizer.clean({:birthday => '1986-10-06'})
|
54
|
+
# => {:birthday => Date.new(1986, 10, 6)}
|
55
|
+
|
56
|
+
# it prevents obvious errors
|
57
|
+
data = PrivilegedSanitizer.clean({:account_id => 3})
|
58
|
+
data[:account] # instead of :account_id
|
59
|
+
# => InputSanitizer::KeyNotAllowedError: Key not allowed: account
|
60
|
+
# => ...
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
1. Fork it
|
67
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
68
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
69
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
70
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'input_sanitizer/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.authors = ["Tomek Paczkowski", "Tomasz Werbicki", "Michal Bugno"]
|
7
|
+
gem.email = ["tom@futuresimple.com", "tomasz@futuresimple.com", "michal@futuresimple.com"]
|
8
|
+
gem.description = %q{Gem to sanitize hash of incoming data}
|
9
|
+
gem.summary = %q{Gem to sanitize hash of incoming data}
|
10
|
+
gem.homepage = ""
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "input_sanitizer"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = InputSanitizer::VERSION
|
18
|
+
|
19
|
+
gem.add_development_dependency "rspec"
|
20
|
+
gem.add_development_dependency "simplecov"
|
21
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module InputSanitizer
|
4
|
+
class ConversionError < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
class IntegerConverter
|
8
|
+
def call(value)
|
9
|
+
cast = value.to_i
|
10
|
+
if cast.to_s != value.to_s
|
11
|
+
raise ConversionError.new("invalid integer")
|
12
|
+
end
|
13
|
+
cast
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class StringConverter
|
18
|
+
def call(value)
|
19
|
+
value.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class DateConverter
|
24
|
+
ISO_RE = /\A\d{4}-?\d{2}-?\d{2}/
|
25
|
+
|
26
|
+
def call(value)
|
27
|
+
raise ConversionError.new("invalid time") unless value =~ ISO_RE
|
28
|
+
Date.parse(value)
|
29
|
+
rescue ArgumentError
|
30
|
+
raise ConversionError.new("invalid iso8601 date")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TimeConverter
|
35
|
+
ISO_RE = /\A\d{4}-?\d{2}-?\d{2}([T ]?\d{2}(:?\d{2}(:?\d{2})?)?)?\Z/
|
36
|
+
|
37
|
+
def call(value)
|
38
|
+
if value =~ ISO_RE
|
39
|
+
strip_timezone(Time.parse(value))
|
40
|
+
else
|
41
|
+
raise ConversionError.new("invalid time")
|
42
|
+
end
|
43
|
+
rescue ArgumentError
|
44
|
+
raise ConversionError.new("invalid time")
|
45
|
+
end
|
46
|
+
|
47
|
+
def strip_timezone(time)
|
48
|
+
Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class BooleanConverter
|
53
|
+
BOOLEAN_MAP = {
|
54
|
+
true => true,
|
55
|
+
false => false,
|
56
|
+
'true' => true,
|
57
|
+
'false' => false,
|
58
|
+
'1' => true,
|
59
|
+
'0' => false,
|
60
|
+
'yes' => true,
|
61
|
+
'no' => false,
|
62
|
+
}
|
63
|
+
|
64
|
+
def call(value)
|
65
|
+
if BOOLEAN_MAP.has_key?(value)
|
66
|
+
BOOLEAN_MAP[value]
|
67
|
+
else
|
68
|
+
truthy, falsy = BOOLEAN_MAP.partition { |_, value| value }
|
69
|
+
truthy = truthy.map { |e| "'#{e[0]}'" }.uniq
|
70
|
+
falsy = falsy.map { |e| "'#{e[0]}'" }.uniq
|
71
|
+
|
72
|
+
message = "Invalid boolean: use "
|
73
|
+
message += truthy.join(", ")
|
74
|
+
message += " for true, or "
|
75
|
+
message += falsy.join(", ")
|
76
|
+
message += " for false."
|
77
|
+
raise ConversionError.new(message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,42 @@
|
|
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 SpecificValuesConverter
|
24
|
+
def initialize(values)
|
25
|
+
@valid_values = values
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(value)
|
29
|
+
found = @valid_values.include?(value) ? value : nil
|
30
|
+
if !found
|
31
|
+
found = @valid_values.include?(value.to_sym) ? value.to_sym : nil
|
32
|
+
end
|
33
|
+
if !found
|
34
|
+
values_joined = @valid_values.join(", ")
|
35
|
+
error_message = "Possible values: #{values_joined}"
|
36
|
+
raise InputSanitizer::ConversionError.new(error_message)
|
37
|
+
else
|
38
|
+
found
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module InputSanitizer
|
2
|
+
class KeyNotAllowedError < ArgumentError; end
|
3
|
+
|
4
|
+
class RestrictedHash < Hash
|
5
|
+
def initialize(allowed_keys)
|
6
|
+
@allowed_keys = allowed_keys
|
7
|
+
super() { |hash, key| default_for_key(key) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def key_allowed?(key)
|
11
|
+
@allowed_keys.include?(key)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def default_for_key(key)
|
16
|
+
key_allowed?(key) ? nil : raise_not_allowed(key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def raise_not_allowed(key)
|
20
|
+
msg = "Key not allowed: #{key}"
|
21
|
+
raise KeyNotAllowedError.new(msg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'input_sanitizer/restricted_hash'
|
2
|
+
require 'input_sanitizer/default_converters'
|
3
|
+
|
4
|
+
class InputSanitizer::Sanitizer
|
5
|
+
def initialize(data)
|
6
|
+
@data = symbolize_keys(data)
|
7
|
+
@performed = false
|
8
|
+
@errors = []
|
9
|
+
@cleaned = InputSanitizer::RestrictedHash.new(self.class.fields.keys)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.clean(data)
|
13
|
+
new(data).cleaned
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](field)
|
17
|
+
cleaned[field]
|
18
|
+
end
|
19
|
+
|
20
|
+
def cleaned
|
21
|
+
return @cleaned if @performed
|
22
|
+
self.class.fields.each do |field, hash|
|
23
|
+
type = hash[:type]
|
24
|
+
required = hash[:options][:required]
|
25
|
+
clean_field(field, type, required)
|
26
|
+
end
|
27
|
+
@performed = true
|
28
|
+
@cleaned.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid?
|
32
|
+
cleaned
|
33
|
+
@errors.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def errors
|
37
|
+
cleaned
|
38
|
+
@errors
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.converters
|
42
|
+
{
|
43
|
+
:integer => InputSanitizer::IntegerConverter.new,
|
44
|
+
:string => InputSanitizer::StringConverter.new,
|
45
|
+
:date => InputSanitizer::DateConverter.new,
|
46
|
+
:time => InputSanitizer::TimeConverter.new,
|
47
|
+
:boolean => InputSanitizer::BooleanConverter.new,
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.inherited(subclass)
|
52
|
+
subclass.fields = self.fields.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.string(*keys)
|
56
|
+
set_keys_to_type(keys, :string)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.integer(*keys)
|
60
|
+
set_keys_to_type(keys, :integer)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.boolean(*keys)
|
64
|
+
set_keys_to_type(keys, :boolean)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.date(*keys)
|
68
|
+
set_keys_to_type(keys, :date)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.time(*keys)
|
72
|
+
set_keys_to_type(keys, :time)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.custom(*keys)
|
76
|
+
options = keys.pop
|
77
|
+
converter = options.delete(:converter)
|
78
|
+
keys.push(options)
|
79
|
+
raise "You did not define a converter for a custom type" if converter == nil
|
80
|
+
self.set_keys_to_type(keys, converter)
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
def self.fields
|
85
|
+
@fields ||= {}
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.fields=(new_fields)
|
89
|
+
@fields = new_fields
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
def self.extract_options!(array)
|
94
|
+
array.last.is_a?(Hash) ? array.pop : {}
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.extract_options(array)
|
98
|
+
array.last.is_a?(Hash) ? array.last : {}
|
99
|
+
end
|
100
|
+
|
101
|
+
def clean_field(field, type, required)
|
102
|
+
if @data.has_key?(field)
|
103
|
+
begin
|
104
|
+
@cleaned[field] = convert(field, type)
|
105
|
+
rescue InputSanitizer::ConversionError => ex
|
106
|
+
add_error(field, :invalid_value, @data[field], ex.message)
|
107
|
+
end
|
108
|
+
elsif required
|
109
|
+
add_missing(field)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_error(field, error_type, value, description = nil)
|
114
|
+
@errors << {
|
115
|
+
:field => field,
|
116
|
+
:type => error_type,
|
117
|
+
:value => value,
|
118
|
+
:description => description
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_missing(field)
|
123
|
+
add_error(field, :missing, nil, nil)
|
124
|
+
end
|
125
|
+
|
126
|
+
def convert(field, type)
|
127
|
+
converter(type).call(@data[field])
|
128
|
+
end
|
129
|
+
|
130
|
+
def converter(type)
|
131
|
+
type.respond_to?(:call) ? type : self.class.converters[type]
|
132
|
+
end
|
133
|
+
|
134
|
+
def symbolize_keys(data)
|
135
|
+
data.inject({}) do |memo, kv|
|
136
|
+
memo[kv.first.to_sym] = kv.last
|
137
|
+
memo
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.set_keys_to_type(keys, type)
|
142
|
+
opts = extract_options!(keys)
|
143
|
+
keys.each do |key|
|
144
|
+
fields[key] = { :type => type, :options => opts }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe InputSanitizer::IntegerConverter do
|
4
|
+
let(:converter) { InputSanitizer::IntegerConverter.new }
|
5
|
+
|
6
|
+
it "casts string to integer" do
|
7
|
+
converter.call("42").should == 42
|
8
|
+
end
|
9
|
+
|
10
|
+
it "casts integer to integer" do
|
11
|
+
converter.call(42).should == 42
|
12
|
+
end
|
13
|
+
|
14
|
+
it "raises error if cannot cast" do
|
15
|
+
lambda { converter.call("f") }.should raise_error(InputSanitizer::ConversionError)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe InputSanitizer::DateConverter do
|
20
|
+
let(:converter) { InputSanitizer::DateConverter.new }
|
21
|
+
|
22
|
+
it "casts dates in iso format" do
|
23
|
+
converter.call("2012-05-15").should == Date.new(2012, 5, 15)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "raises error if cannot cast" do
|
27
|
+
lambda { converter.call("2012-02-30") }.should raise_error(InputSanitizer::ConversionError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe InputSanitizer::BooleanConverter do
|
32
|
+
let(:converter) { InputSanitizer::BooleanConverter.new }
|
33
|
+
|
34
|
+
it "casts 'true' to true" do
|
35
|
+
converter.call('true').should be_true
|
36
|
+
end
|
37
|
+
|
38
|
+
it "casts true to true" do
|
39
|
+
converter.call(true).should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "casts '1' to true" do
|
43
|
+
converter.call('1').should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it "casts 'yes' to true" do
|
47
|
+
converter.call('yes').should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "casts 'false' to false" do
|
51
|
+
converter.call('false').should be_false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "casts false to false" do
|
55
|
+
converter.call(false).should be_false
|
56
|
+
end
|
57
|
+
|
58
|
+
it "casts '0' to false" do
|
59
|
+
converter.call('0').should be_false
|
60
|
+
end
|
61
|
+
|
62
|
+
it "casts 'no' to false" do
|
63
|
+
converter.call('no').should be_false
|
64
|
+
end
|
65
|
+
|
66
|
+
it "raises error if cannot cast" do
|
67
|
+
lambda { converter.call("notboolean") }.should raise_error(InputSanitizer::ConversionError)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
describe InputSanitizer::TimeConverter do
|
73
|
+
let(:converter) { InputSanitizer::TimeConverter.new }
|
74
|
+
|
75
|
+
it "raises if timezone part given" do
|
76
|
+
lambda { converter.call("2012-05-15 13:42:54 +01:00") }.should raise_error(InputSanitizer::ConversionError)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "casts date time in iso format" do
|
80
|
+
t = Time.utc(2012, 5, 15, 13, 42, 54)
|
81
|
+
converter.call("2012-05-15 13:42:54").should == t
|
82
|
+
converter.call("2012-05-15T13:42:54").should == t
|
83
|
+
converter.call("20120515134254").should == t
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not require time part" do
|
88
|
+
converter.call("2012-05-15 13:42").should == Time.utc(2012, 5, 15, 13, 42)
|
89
|
+
converter.call("2012-05-15 13").should == Time.utc(2012, 5, 15, 13)
|
90
|
+
converter.call("2012-05-15").should == Time.utc(2012, 5, 15)
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
it "raises error if can format is wrong" do
|
95
|
+
lambda { converter.call("2/10/2031 13:44:22") }.should raise_error(InputSanitizer::ConversionError)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "raises error if date is wrong" do
|
99
|
+
lambda { converter.call("2012-02-32") }.should raise_error(InputSanitizer::ConversionError)
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'input_sanitizer/extended_converters'
|
3
|
+
|
4
|
+
describe InputSanitizer::PositiveIntegerConverter do
|
5
|
+
let(:converter) { InputSanitizer::PositiveIntegerConverter.new }
|
6
|
+
|
7
|
+
it "raises error if integer less than zero" do
|
8
|
+
lambda { converter.call("-3") }.should raise_error(InputSanitizer::ConversionError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "raises error if integer equals zero" do
|
12
|
+
lambda { converter.call("0") }.should raise_error(InputSanitizer::ConversionError)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe InputSanitizer::CommaJoinedIntegersConverter do
|
17
|
+
let(:converter) { InputSanitizer::CommaJoinedIntegersConverter.new }
|
18
|
+
|
19
|
+
it "parses to array of ids" do
|
20
|
+
converter.call("1,2,3,5").should == [1, 2, 3, 5]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "raises on invalid character" do
|
24
|
+
lambda { converter.call(":") }.should raise_error(InputSanitizer::ConversionError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe InputSanitizer::SpecificValuesConverter do
|
29
|
+
let(:converter) { InputSanitizer::SpecificValuesConverter.new([:a, :b]) }
|
30
|
+
|
31
|
+
it "converts valid value to symbol" do
|
32
|
+
converter.call("b").should == :b
|
33
|
+
end
|
34
|
+
|
35
|
+
it "raises on invalid value" do
|
36
|
+
lambda { converter.call("c") }.should raise_error(InputSanitizer::ConversionError)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "converts valid value to string" do
|
40
|
+
converter = InputSanitizer::SpecificValuesConverter.new(["a", "b"])
|
41
|
+
converter.call("a").should == "a"
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe InputSanitizer::RestrictedHash do
|
4
|
+
let(:hash) { InputSanitizer::RestrictedHash.new([:a, :b]) }
|
5
|
+
subject { hash }
|
6
|
+
|
7
|
+
it "does not allow bad keys" do
|
8
|
+
lambda{hash[:c]}.should raise_error(InputSanitizer::KeyNotAllowedError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "does allow correct keys" do
|
12
|
+
hash[:a].should be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns value for correct key" do
|
16
|
+
hash[:a] = 'stuff'
|
17
|
+
hash[:a].should == 'stuff'
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class BasicSanitizer < InputSanitizer::Sanitizer
|
4
|
+
string :x, :y, :z
|
5
|
+
integer :num
|
6
|
+
date :birthday
|
7
|
+
time :updated_at
|
8
|
+
custom :cust1, :cust2, :converter => lambda { |v| v.reverse }
|
9
|
+
end
|
10
|
+
|
11
|
+
class BrokenCustomSanitizer < InputSanitizer::Sanitizer
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class ExtendedSanitizer < BasicSanitizer
|
16
|
+
boolean :is_nice
|
17
|
+
end
|
18
|
+
|
19
|
+
class OverridingSanitizer < BasicSanitizer
|
20
|
+
integer :is_nice
|
21
|
+
end
|
22
|
+
|
23
|
+
class RequiredParameters < BasicSanitizer
|
24
|
+
integer :is_nice, :required => true
|
25
|
+
end
|
26
|
+
|
27
|
+
class RequiredCustom < BasicSanitizer
|
28
|
+
custom :c1, :required => true, :converter => lambda { |v| v }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe InputSanitizer::Sanitizer do
|
32
|
+
let(:sanitizer) { BasicSanitizer.new(@params) }
|
33
|
+
|
34
|
+
describe ".clean" do
|
35
|
+
it "returns cleaned data" do
|
36
|
+
clean_data = mock()
|
37
|
+
BasicSanitizer.any_instance.should_receive(:cleaned).and_return(clean_data)
|
38
|
+
BasicSanitizer.clean({}).should be(clean_data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#cleaned" do
|
43
|
+
let(:cleaned) { sanitizer.cleaned }
|
44
|
+
let(:required) { RequiredParameters.new(@params) }
|
45
|
+
|
46
|
+
it "includes specified params" do
|
47
|
+
@params = {"x" => 3, "y" => "tom", "z" => "mike"}
|
48
|
+
|
49
|
+
cleaned.should have_key(:x)
|
50
|
+
cleaned.should have_key(:y)
|
51
|
+
cleaned.should have_key(:z)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "strips not specified params" do
|
55
|
+
@params = {"d" => 3}
|
56
|
+
|
57
|
+
cleaned.should_not have_key(:d)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "freezes cleaned hash" do
|
61
|
+
@params = {}
|
62
|
+
|
63
|
+
cleaned.should be_frozen
|
64
|
+
end
|
65
|
+
|
66
|
+
it "uses RestrictedHash" do
|
67
|
+
@params = {}
|
68
|
+
|
69
|
+
lambda{cleaned[:does_not_exist]}.should raise_error(InputSanitizer::KeyNotAllowedError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "includes specified keys and strips rest" do
|
73
|
+
@params = {"d" => 3, "x" => "ddd"}
|
74
|
+
|
75
|
+
cleaned.should have_key(:x)
|
76
|
+
cleaned.should_not have_key(:d)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "works with symbols as input keys" do
|
80
|
+
@params = {:d => 3, :x => "ddd"}
|
81
|
+
|
82
|
+
cleaned.should have_key(:x)
|
83
|
+
cleaned.should_not have_key(:d)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "silently discards cast errors" do
|
87
|
+
@params = {:num => "f"}
|
88
|
+
|
89
|
+
cleaned.should_not have_key(:num)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "inherits converters from superclass" do
|
93
|
+
sanitizer = ExtendedSanitizer.new({:num => "23", :is_nice => 'false'})
|
94
|
+
cleaned = sanitizer.cleaned
|
95
|
+
|
96
|
+
cleaned.should have_key(:num)
|
97
|
+
cleaned[:num].should == 23
|
98
|
+
cleaned[:is_nice].should be_false
|
99
|
+
end
|
100
|
+
|
101
|
+
it "overrides inherited fields" do
|
102
|
+
sanitizer = OverridingSanitizer.new({:is_nice => "42"})
|
103
|
+
cleaned = sanitizer.cleaned
|
104
|
+
|
105
|
+
cleaned.should have_key(:is_nice)
|
106
|
+
cleaned[:is_nice].should == 42
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
describe ".custom" do
|
112
|
+
let(:sanitizer) { BasicSanitizer.new(@params) }
|
113
|
+
let(:cleaned) { sanitizer.cleaned }
|
114
|
+
|
115
|
+
it "converts using custom converter" do
|
116
|
+
@params = {:cust1 => "cigam"}
|
117
|
+
|
118
|
+
cleaned.should have_key(:cust1)
|
119
|
+
cleaned[:cust1].should == "magic"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "raises an error when converter is not defined" do
|
123
|
+
expect do
|
124
|
+
BrokenCustomSanitizer.custom(:x)
|
125
|
+
end.should raise_error
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe ".converters" do
|
130
|
+
let(:sanitizer) { InputSanitizer::Sanitizer }
|
131
|
+
|
132
|
+
it "includes :integer type" do
|
133
|
+
sanitizer.converters.should have_key(:integer)
|
134
|
+
sanitizer.converters[:integer].should be_a(InputSanitizer::IntegerConverter)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "includes :string type" do
|
138
|
+
sanitizer.converters.should have_key(:string)
|
139
|
+
sanitizer.converters[:string].should be_a(InputSanitizer::StringConverter)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "includes :date type" do
|
143
|
+
sanitizer.converters.should have_key(:date)
|
144
|
+
sanitizer.converters[:date].should be_a(InputSanitizer::DateConverter)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "includes :boolean type" do
|
148
|
+
sanitizer.converters.should have_key(:boolean)
|
149
|
+
sanitizer.converters[:boolean].should be_a(InputSanitizer::BooleanConverter)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '.extract_options' do
|
154
|
+
|
155
|
+
it "extracts hash from array if is last" do
|
156
|
+
options = { :a => 1}
|
157
|
+
array = [1,2, options]
|
158
|
+
BasicSanitizer.extract_options(array).should == options
|
159
|
+
array.should == [1,2, options]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "does not extract the last element if not a hash and returns default empty hash" do
|
163
|
+
array = [1,2]
|
164
|
+
BasicSanitizer.extract_options(array).should_not == 2
|
165
|
+
BasicSanitizer.extract_options(array).should == {}
|
166
|
+
array.should == [1,2]
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
describe '.extract_options!' do
|
172
|
+
|
173
|
+
it "extracts hash from array if is last" do
|
174
|
+
options = { :a => 1}
|
175
|
+
array = [1,2, options]
|
176
|
+
BasicSanitizer.extract_options!(array).should == options
|
177
|
+
array.should == [1,2]
|
178
|
+
end
|
179
|
+
|
180
|
+
it "leaves other arrays alone" do
|
181
|
+
array = [1,2]
|
182
|
+
BasicSanitizer.extract_options!(array).should == {}
|
183
|
+
array.should == [1,2]
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#valid?" do
|
189
|
+
it "is valid when params are ok" do
|
190
|
+
@params = {:num => "3"}
|
191
|
+
|
192
|
+
sanitizer.should be_valid
|
193
|
+
end
|
194
|
+
|
195
|
+
it "is not valid when missing params" do
|
196
|
+
@params = {:num => "mike"}
|
197
|
+
|
198
|
+
sanitizer.should_not be_valid
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe "#[]" do
|
203
|
+
it "accesses cleaned data" do
|
204
|
+
@params = {:num => "3"}
|
205
|
+
|
206
|
+
sanitizer[:num].should == 3
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "#errors" do
|
211
|
+
it "returns array containing hashes describing error" do
|
212
|
+
@params = {:num => "mike"}
|
213
|
+
|
214
|
+
errors = sanitizer.errors
|
215
|
+
errors.size.should == 1
|
216
|
+
errors[0][:field].should == :num
|
217
|
+
errors[0][:type].should == :invalid_value
|
218
|
+
errors[0][:description].should == "invalid integer"
|
219
|
+
errors[0][:value].should == "mike"
|
220
|
+
end
|
221
|
+
|
222
|
+
it "returns error type missing if value is missing" do
|
223
|
+
sanitizer = RequiredParameters.new({})
|
224
|
+
error = sanitizer.errors[0]
|
225
|
+
error[:type].should == :missing
|
226
|
+
end
|
227
|
+
|
228
|
+
it "handles required custom params" do
|
229
|
+
sanitizer = RequiredCustom.new({})
|
230
|
+
|
231
|
+
sanitizer.should_not be_valid
|
232
|
+
error = sanitizer.errors[0]
|
233
|
+
error[:type].should == :missing
|
234
|
+
error[:field].should == :c1
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: input_sanitizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.8
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tomek Paczkowski
|
9
|
+
- Tomasz Werbicki
|
10
|
+
- Michal Bugno
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-10-09 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: simplecov
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
description: Gem to sanitize hash of incoming data
|
49
|
+
email:
|
50
|
+
- tom@futuresimple.com
|
51
|
+
- tomasz@futuresimple.com
|
52
|
+
- michal@futuresimple.com
|
53
|
+
executables: []
|
54
|
+
extensions: []
|
55
|
+
extra_rdoc_files: []
|
56
|
+
files:
|
57
|
+
- .gitignore
|
58
|
+
- .rspec
|
59
|
+
- .travis.yml
|
60
|
+
- Gemfile
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- input_sanitizer.gemspec
|
65
|
+
- lib/input_sanitizer.rb
|
66
|
+
- lib/input_sanitizer/default_converters.rb
|
67
|
+
- lib/input_sanitizer/extended_converters.rb
|
68
|
+
- lib/input_sanitizer/restricted_hash.rb
|
69
|
+
- lib/input_sanitizer/sanitizer.rb
|
70
|
+
- lib/input_sanitizer/version.rb
|
71
|
+
- spec/default_converters_spec.rb
|
72
|
+
- spec/extended_converters_spec.rb
|
73
|
+
- spec/restricted_hash_spec.rb
|
74
|
+
- spec/sanitizer_spec.rb
|
75
|
+
- spec/spec_helper.rb
|
76
|
+
homepage: ''
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Gem to sanitize hash of incoming data
|
100
|
+
test_files:
|
101
|
+
- spec/default_converters_spec.rb
|
102
|
+
- spec/extended_converters_spec.rb
|
103
|
+
- spec/restricted_hash_spec.rb
|
104
|
+
- spec/sanitizer_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
has_rdoc:
|