mutations 0.5.12 → 0.6.0

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 (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