mutations 0.5.12 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/{.travis → .travis.yml} +3 -0
  2. data/CHANGELOG.md +10 -2
  3. data/Gemfile +5 -2
  4. data/README.md +1 -0
  5. data/TODO +1 -3
  6. data/lib/mutations.rb +14 -1
  7. data/lib/mutations/additional_filter.rb +13 -0
  8. data/lib/mutations/array_filter.rb +12 -30
  9. data/lib/mutations/boolean_filter.rb +3 -3
  10. data/lib/mutations/command.rb +5 -4
  11. data/lib/mutations/date_filter.rb +52 -0
  12. data/lib/mutations/duck_filter.rb +4 -4
  13. data/lib/mutations/errors.rb +24 -20
  14. data/lib/mutations/exception.rb +1 -1
  15. data/lib/mutations/file_filter.rb +7 -7
  16. data/lib/mutations/float_filter.rb +5 -5
  17. data/lib/mutations/hash_filter.rb +12 -29
  18. data/lib/mutations/integer_filter.rb +5 -5
  19. data/lib/mutations/model_filter.rb +13 -8
  20. data/lib/mutations/outcome.rb +1 -1
  21. data/lib/mutations/string_filter.rb +10 -10
  22. data/lib/mutations/version.rb +2 -2
  23. data/mutations.gemspec +0 -2
  24. data/spec/additional_filter_spec.rb +76 -0
  25. data/spec/array_filter_spec.rb +25 -17
  26. data/spec/boolean_filter_spec.rb +5 -5
  27. data/spec/command_spec.rb +30 -30
  28. data/spec/date_filter_spec.rb +146 -0
  29. data/spec/default_spec.rb +10 -10
  30. data/spec/duck_filter_spec.rb +6 -6
  31. data/spec/errors_spec.rb +8 -9
  32. data/spec/file_filter_spec.rb +26 -22
  33. data/spec/float_filter_spec.rb +8 -8
  34. data/spec/hash_filter_spec.rb +31 -23
  35. data/spec/inheritance_spec.rb +9 -9
  36. data/spec/integer_filter_spec.rb +9 -9
  37. data/spec/model_filter_spec.rb +26 -6
  38. data/spec/mutations_spec.rb +1 -1
  39. data/spec/simple_command.rb +2 -2
  40. data/spec/spec_helper.rb +1 -1
  41. data/spec/string_filter_spec.rb +17 -17
  42. metadata +8 -4
@@ -1,7 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 1.8.7
3
4
  - 1.9.3
4
5
  - jruby-19mode
6
+ - rbx-19mode
7
+ - 2.0.0
5
8
  branches:
6
9
  only:
7
10
  - master
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
@@ -1,5 +1,8 @@
1
1
  source 'http://rubygems.org'
2
2
  gemspec
3
3
 
4
- gem 'minitest', '~> 4.0'
5
- gem 'activesupport'
4
+ gem 'activesupport'
5
+
6
+ group :test do
7
+ gem 'minitest', '~> 4.0'
8
+ end
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: false, # true allows an explicit nil to be valid. Overrides any other options
5
- class: nil, # A constant or string indicates that each element of the array needs to be one of these classes
6
- arrayize: false # true will convert "hi" to ["hi"]. "" converts to []
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: i) if el_error.is_a?(Symbol)
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 < InputFilter
2
+ class BooleanFilter < AdditionalFilter
3
3
  @default_options = {
4
- nils: false # true allows an explicit nil to be valid. Overrides any other options
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
@@ -55,11 +55,11 @@ module Mutations
55
55
 
56
56
  # Instance methods
57
57
  def initialize(*args)
58
- @raw_inputs = args.each_with_object({}.with_indifferent_access) do |arg, h|
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
- *path, last = key.to_s.split(".")
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: 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 < InputFilter
2
+ class DuckFilter < AdditionalFilter
3
3
  @default_options = {
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.
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
@@ -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: "can't be nil",
10
- required: "is required",
9
+ :nils => "can't be nil",
10
+ :required => "is required",
11
11
 
12
12
  # Datatypes
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",
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: "can't be blank",
22
- max_length: "is too long",
23
- min_length: "is too short",
24
- matches: "isn't in the right format",
25
- in: "isn't an option",
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: "isn't the right class",
33
+ :class => "isn't the right class",
29
34
 
30
35
  # Integer
31
- min: "is too small",
32
- max: "is too big",
36
+ :min => "is too small",
37
+ :max => "is too big",
33
38
 
34
39
  # Model
35
- new_records: "isn't a saved model"
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: @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
@@ -10,4 +10,4 @@ module Mutations
10
10
  "#{self.errors.message_list.join('; ')}"
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -1,9 +1,9 @@
1
1
  module Mutations
2
- class FileFilter < InputFilter
2
+ class FileFilter < AdditionalFilter
3
3
  @default_options = {
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
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 < InputFilter
2
+ class FloatFilter < AdditionalFilter
3
3
  @default_options = {
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
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