mutations 0.5.12 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{.travis → .travis.yml} +3 -0
- data/CHANGELOG.md +10 -2
- data/Gemfile +5 -2
- data/README.md +1 -0
- data/TODO +1 -3
- data/lib/mutations.rb +14 -1
- data/lib/mutations/additional_filter.rb +13 -0
- data/lib/mutations/array_filter.rb +12 -30
- data/lib/mutations/boolean_filter.rb +3 -3
- data/lib/mutations/command.rb +5 -4
- data/lib/mutations/date_filter.rb +52 -0
- data/lib/mutations/duck_filter.rb +4 -4
- data/lib/mutations/errors.rb +24 -20
- data/lib/mutations/exception.rb +1 -1
- data/lib/mutations/file_filter.rb +7 -7
- data/lib/mutations/float_filter.rb +5 -5
- data/lib/mutations/hash_filter.rb +12 -29
- data/lib/mutations/integer_filter.rb +5 -5
- data/lib/mutations/model_filter.rb +13 -8
- data/lib/mutations/outcome.rb +1 -1
- data/lib/mutations/string_filter.rb +10 -10
- data/lib/mutations/version.rb +2 -2
- data/mutations.gemspec +0 -2
- data/spec/additional_filter_spec.rb +76 -0
- data/spec/array_filter_spec.rb +25 -17
- data/spec/boolean_filter_spec.rb +5 -5
- data/spec/command_spec.rb +30 -30
- data/spec/date_filter_spec.rb +146 -0
- data/spec/default_spec.rb +10 -10
- data/spec/duck_filter_spec.rb +6 -6
- data/spec/errors_spec.rb +8 -9
- data/spec/file_filter_spec.rb +26 -22
- data/spec/float_filter_spec.rb +8 -8
- data/spec/hash_filter_spec.rb +31 -23
- data/spec/inheritance_spec.rb +9 -9
- data/spec/integer_filter_spec.rb +9 -9
- data/spec/model_filter_spec.rb +26 -6
- data/spec/mutations_spec.rb +1 -1
- data/spec/simple_command.rb +2 -2
- data/spec/spec_helper.rb +1 -1
- data/spec/string_filter_spec.rb +17 -17
- metadata +8 -4
data/{.travis → .travis.yml}
RENAMED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
0.6.0
|
2
|
+
-----------
|
3
|
+
|
4
|
+
- Add pluggable filters.
|
5
|
+
- Add ruby 1.8.7 support [#19]
|
6
|
+
- Add a date filter: ```date :start_date``` (/via @eliank and @geronimo)
|
7
|
+
- ```Mutations.cache_constants = false``` if you want to work in Rails dev mode which redefines constants. [#23]
|
8
|
+
|
1
9
|
0.5.12
|
2
10
|
-----------
|
3
11
|
|
@@ -15,8 +23,8 @@
|
|
15
23
|
- Model filters should lazily resolve their classes so that mocks can be used (@edwinv)
|
16
24
|
- Filtered inputs should be included in the Outcome
|
17
25
|
- Fix typos (@frodsan)
|
18
|
-
|
26
|
+
|
19
27
|
0.5.10 and earlier
|
20
28
|
-----------
|
21
29
|
|
22
|
-
- Initial versions
|
30
|
+
- Initial versions
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Mutations
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/cypriss/mutations.png)](https://travis-ci.org/cypriss/mutations)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/cypriss/mutations.png)](https://codeclimate.com/github/cypriss/mutations)
|
4
5
|
|
5
6
|
Compose your business logic into commands that sanitize and validate input. Write safe, reusable, and maintainable code for Ruby and Rails apps.
|
6
7
|
|
data/TODO
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
- Document default behavior discarding of nils for optional params
|
2
2
|
- document string.discard_empty
|
3
|
-
|
3
|
+
|
4
4
|
- protected parameters:
|
5
5
|
optional do
|
6
6
|
boolean :skip_confirmation, protected: true
|
@@ -10,5 +10,3 @@
|
|
10
10
|
But this would not:
|
11
11
|
params = {user: current_user, skip_confirmation: true}
|
12
12
|
User::ChangeEmail.run!(params)
|
13
|
-
|
14
|
-
|
data/lib/mutations.rb
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
require 'active_support/core_ext/hash/indifferent_access'
|
3
3
|
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'date'
|
4
5
|
|
5
6
|
require 'mutations/version'
|
6
7
|
require 'mutations/exception'
|
7
8
|
require 'mutations/errors'
|
8
9
|
require 'mutations/input_filter'
|
10
|
+
require 'mutations/additional_filter'
|
9
11
|
require 'mutations/string_filter'
|
10
12
|
require 'mutations/integer_filter'
|
11
13
|
require 'mutations/float_filter'
|
12
14
|
require 'mutations/boolean_filter'
|
13
15
|
require 'mutations/duck_filter'
|
16
|
+
require 'mutations/date_filter'
|
14
17
|
require 'mutations/file_filter'
|
15
18
|
require 'mutations/model_filter'
|
16
19
|
require 'mutations/array_filter'
|
@@ -27,5 +30,15 @@ module Mutations
|
|
27
30
|
def error_message_creator=(creator)
|
28
31
|
@error_message_creator = creator
|
29
32
|
end
|
33
|
+
|
34
|
+
def cache_constants=(val)
|
35
|
+
@cache_constants = val
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_constants?
|
39
|
+
@cache_constants
|
40
|
+
end
|
30
41
|
end
|
31
|
-
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Mutations.cache_constants = true
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'mutations/hash_filter'
|
2
|
+
require 'mutations/array_filter'
|
3
|
+
|
4
|
+
module Mutations
|
5
|
+
class AdditionalFilter < InputFilter
|
6
|
+
def self.inherited(subclass)
|
7
|
+
type_name = subclass.name[/^Mutations::([a-zA-Z]*)Filter$/, 1].downcase
|
8
|
+
|
9
|
+
Mutations::HashFilter.register_additional_filter(subclass, type_name)
|
10
|
+
Mutations::ArrayFilter.register_additional_filter(subclass, type_name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,9 +1,16 @@
|
|
1
1
|
module Mutations
|
2
2
|
class ArrayFilter < InputFilter
|
3
|
+
def self.register_additional_filter(type_class, type_name)
|
4
|
+
define_method(type_name) do | *args |
|
5
|
+
options = args[0] || {}
|
6
|
+
@element_filter = type_class.new(options)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
3
10
|
@default_options = {
|
4
|
-
nils
|
5
|
-
class
|
6
|
-
arrayize
|
11
|
+
:nils => false, # true allows an explicit nil to be valid. Overrides any other options
|
12
|
+
:class => nil, # A constant or string indicates that each element of the array needs to be one of these classes
|
13
|
+
:arrayize => false # true will convert "hi" to ["hi"]. "" converts to []
|
7
14
|
}
|
8
15
|
|
9
16
|
def initialize(name, opts = {}, &block)
|
@@ -19,35 +26,10 @@ module Mutations
|
|
19
26
|
raise ArgumentError.new("Can't supply both a class and a filter") if @element_filter && self.options[:class]
|
20
27
|
end
|
21
28
|
|
22
|
-
def string(options = {})
|
23
|
-
@element_filter = StringFilter.new(options)
|
24
|
-
end
|
25
|
-
|
26
|
-
def integer(options = {})
|
27
|
-
@element_filter = IntegerFilter.new(options)
|
28
|
-
end
|
29
|
-
|
30
|
-
def float(options = {})
|
31
|
-
@element_filter = FloatFilter.new(options)
|
32
|
-
end
|
33
|
-
|
34
|
-
def boolean(options = {})
|
35
|
-
@element_filter = BooleanFilter.new(options)
|
36
|
-
end
|
37
|
-
|
38
|
-
def duck(options = {})
|
39
|
-
@element_filter = DuckFilter.new(options)
|
40
|
-
end
|
41
|
-
|
42
|
-
def file(options = {})
|
43
|
-
@element_filter = FileFilter.new(options)
|
44
|
-
end
|
45
|
-
|
46
29
|
def hash(options = {}, &block)
|
47
30
|
@element_filter = HashFilter.new(options, &block)
|
48
31
|
end
|
49
32
|
|
50
|
-
# Advanced types
|
51
33
|
def model(name, options = {})
|
52
34
|
@element_filter = ModelFilter.new(name.to_sym, options)
|
53
35
|
end
|
@@ -74,7 +56,7 @@ module Mutations
|
|
74
56
|
found_error = false
|
75
57
|
data.each_with_index do |el, i|
|
76
58
|
el_filtered, el_error = filter_element(el)
|
77
|
-
el_error = ErrorAtom.new(@name, el_error, index
|
59
|
+
el_error = ErrorAtom.new(@name, el_error, :index => i) if el_error.is_a?(Symbol)
|
78
60
|
|
79
61
|
errors << el_error
|
80
62
|
found_error = true if el_error
|
@@ -110,4 +92,4 @@ module Mutations
|
|
110
92
|
[data, nil]
|
111
93
|
end
|
112
94
|
end
|
113
|
-
end
|
95
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Mutations
|
2
|
-
class BooleanFilter <
|
2
|
+
class BooleanFilter < AdditionalFilter
|
3
3
|
@default_options = {
|
4
|
-
nils
|
4
|
+
:nils => false # true allows an explicit nil to be valid. Overrides any other options
|
5
5
|
}
|
6
6
|
|
7
7
|
BOOL_MAP = {"true" => true, "1" => true, "false" => false, "0" => false}
|
@@ -30,4 +30,4 @@ module Mutations
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
data/lib/mutations/command.rb
CHANGED
@@ -55,11 +55,11 @@ module Mutations
|
|
55
55
|
|
56
56
|
# Instance methods
|
57
57
|
def initialize(*args)
|
58
|
-
@raw_inputs = args.
|
58
|
+
@raw_inputs = args.inject({}.with_indifferent_access) do |h, arg|
|
59
59
|
raise ArgumentError.new("All arguments must be hashes") unless arg.is_a?(Hash)
|
60
60
|
h.merge!(arg)
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
@inputs, @errors = self.input_filters.filter(@raw_inputs)
|
64
64
|
end
|
65
65
|
|
@@ -110,11 +110,12 @@ module Mutations
|
|
110
110
|
|
111
111
|
@errors ||= ErrorHash.new
|
112
112
|
@errors.tap do |errs|
|
113
|
-
|
113
|
+
path = key.to_s.split(".")
|
114
|
+
last = path.pop
|
114
115
|
inner = path.inject(errs) do |cut_errors,part|
|
115
116
|
cur_errors[part.to_sym] ||= ErrorHash.new
|
116
117
|
end
|
117
|
-
inner[last] = ErrorAtom.new(key, kind, message
|
118
|
+
inner[last] = ErrorAtom.new(key, kind, :message => message)
|
118
119
|
end
|
119
120
|
end
|
120
121
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Mutations
|
2
|
+
class DateFilter < AdditionalFilter
|
3
|
+
@default_options = {
|
4
|
+
:nils => false, # true allows an explicit nil to be valid. Overrides any other options
|
5
|
+
:format => nil, # If nil, Date.parse will be used for coercsion. If something like "%Y-%m-%d", Date.strptime is used
|
6
|
+
:after => nil, # A date object, representing the minimum date allowed, inclusive
|
7
|
+
:before => nil # A date object, representing the maximum date allowed, inclusive
|
8
|
+
}
|
9
|
+
|
10
|
+
def filter(data)
|
11
|
+
# Handle nil case
|
12
|
+
if data.nil?
|
13
|
+
return [nil, nil] if options[:nils]
|
14
|
+
return [nil, :nils]
|
15
|
+
end
|
16
|
+
|
17
|
+
if data.is_a?(Date) # Date and DateTime
|
18
|
+
actual_date = data
|
19
|
+
elsif data.is_a?(String)
|
20
|
+
begin
|
21
|
+
actual_date = if options[:format]
|
22
|
+
Date.strptime(data, options[:format])
|
23
|
+
else
|
24
|
+
Date.parse(data)
|
25
|
+
end
|
26
|
+
rescue ArgumentError
|
27
|
+
return [nil, :date]
|
28
|
+
end
|
29
|
+
elsif data.respond_to?(:to_date) # Time
|
30
|
+
actual_date = data.to_date
|
31
|
+
else
|
32
|
+
return [nil, :date]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Ok, its a valid date, check if it falls within the range
|
36
|
+
if options[:after]
|
37
|
+
if actual_date <= options[:after]
|
38
|
+
return [nil, :after]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if options[:before]
|
43
|
+
if actual_date >= options[:before]
|
44
|
+
return [nil, :before]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# We win, it's valid!
|
49
|
+
[actual_date, nil]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Mutations
|
2
|
-
class DuckFilter <
|
2
|
+
class DuckFilter < AdditionalFilter
|
3
3
|
@default_options = {
|
4
|
-
nils
|
5
|
-
methods
|
4
|
+
:nils => false, # true allows an explicit nil to be valid. Overrides any other options
|
5
|
+
:methods => nil # The object needs to respond to each of the symbols in this array.
|
6
6
|
}
|
7
7
|
|
8
8
|
def filter(data)
|
@@ -22,4 +22,4 @@ module Mutations
|
|
22
22
|
[data, nil]
|
23
23
|
end
|
24
24
|
end
|
25
|
-
end
|
25
|
+
end
|
data/lib/mutations/errors.rb
CHANGED
@@ -6,33 +6,38 @@ module Mutations
|
|
6
6
|
MESSAGES = Hash.new("is invalid").tap do |h|
|
7
7
|
h.merge!(
|
8
8
|
# General
|
9
|
-
nils
|
10
|
-
required
|
9
|
+
:nils => "can't be nil",
|
10
|
+
:required => "is required",
|
11
11
|
|
12
12
|
# Datatypes
|
13
|
-
string
|
14
|
-
integer
|
15
|
-
boolean
|
16
|
-
hash
|
17
|
-
array
|
18
|
-
model
|
13
|
+
:string => "isn't a string",
|
14
|
+
:integer => "isn't an integer",
|
15
|
+
:boolean => "isn't a boolean",
|
16
|
+
:hash => "isn't a hash",
|
17
|
+
:array => "isn't an array",
|
18
|
+
:model => "isn't the right class",
|
19
|
+
|
20
|
+
# Date
|
21
|
+
:date => "date doesn't exist",
|
22
|
+
:before => "isn't before given date",
|
23
|
+
:after => "isn't after given date",
|
19
24
|
|
20
25
|
# String
|
21
|
-
empty
|
22
|
-
max_length
|
23
|
-
min_length
|
24
|
-
matches
|
25
|
-
in
|
26
|
+
:empty => "can't be blank",
|
27
|
+
:max_length => "is too long",
|
28
|
+
:min_length => "is too short",
|
29
|
+
:matches => "isn't in the right format",
|
30
|
+
:in => "isn't an option",
|
26
31
|
|
27
32
|
# Array
|
28
|
-
class
|
33
|
+
:class => "isn't the right class",
|
29
34
|
|
30
35
|
# Integer
|
31
|
-
min
|
32
|
-
max
|
36
|
+
:min => "is too small",
|
37
|
+
:max => "is too big",
|
33
38
|
|
34
39
|
# Model
|
35
|
-
new_records
|
40
|
+
:new_records => "isn't a saved model"
|
36
41
|
)
|
37
42
|
end
|
38
43
|
|
@@ -49,7 +54,6 @@ module Mutations
|
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
52
|
-
|
53
57
|
class ErrorAtom
|
54
58
|
|
55
59
|
# NOTE: in the future, could also pass in:
|
@@ -70,7 +74,7 @@ module Mutations
|
|
70
74
|
end
|
71
75
|
|
72
76
|
def message
|
73
|
-
@message ||= Mutations.error_message_creator.message(@key, @symbol, index
|
77
|
+
@message ||= Mutations.error_message_creator.message(@key, @symbol, :index => @index)
|
74
78
|
end
|
75
79
|
|
76
80
|
def message_list
|
@@ -152,4 +156,4 @@ module Mutations
|
|
152
156
|
compact.map {|e| e.message_list }.flatten
|
153
157
|
end
|
154
158
|
end
|
155
|
-
end
|
159
|
+
end
|
data/lib/mutations/exception.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Mutations
|
2
|
-
class FileFilter <
|
2
|
+
class FileFilter < AdditionalFilter
|
3
3
|
@default_options = {
|
4
|
-
nils
|
5
|
-
upload
|
6
|
-
size
|
4
|
+
:nils => false, # true allows an explicit nil to be valid. Overrides any other options
|
5
|
+
:upload => false, # if true, also checks the file is has original_filename and content_type methods.
|
6
|
+
:size => nil # An integer value like 1_000_000 limits the size of the file to 1M bytes
|
7
7
|
}
|
8
8
|
|
9
9
|
def filter(data)
|
@@ -20,13 +20,13 @@ module Mutations
|
|
20
20
|
methods.each do |method|
|
21
21
|
return [data, :file] unless data.respond_to?(method)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
if options[:size].is_a?(Fixnum)
|
25
|
-
return [data, :size] if data.size > options[:size]
|
25
|
+
return [data, :size] if data.size > options[:size]
|
26
26
|
end
|
27
27
|
|
28
28
|
# We win, it's valid!
|
29
29
|
[data, nil]
|
30
30
|
end
|
31
31
|
end
|
32
|
-
end
|
32
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Mutations
|
2
|
-
class FloatFilter <
|
2
|
+
class FloatFilter < AdditionalFilter
|
3
3
|
@default_options = {
|
4
|
-
nils
|
5
|
-
min
|
6
|
-
max
|
4
|
+
:nils => false, # true allows an explicit nil to be valid. Overrides any other options
|
5
|
+
:min => nil, # lowest value, inclusive
|
6
|
+
:max => nil # highest value, inclusive
|
7
7
|
}
|
8
8
|
|
9
9
|
def filter(data)
|
@@ -32,4 +32,4 @@ module Mutations
|
|
32
32
|
[data, nil]
|
33
33
|
end
|
34
34
|
end
|
35
|
-
end
|
35
|
+
end
|