mutations 0.5.9 → 0.5.10

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/README.md CHANGED
@@ -7,7 +7,7 @@ Compose your business logic into commands that sanitize and validate input. Writ
7
7
  ## Installation
8
8
 
9
9
  gem install mutations
10
-
10
+
11
11
  Or add it to your Gemfile:
12
12
 
13
13
  gem 'mutations'
@@ -23,12 +23,12 @@ class UserSignup < Mutations::Command
23
23
  string :email, matches: EMAIL_REGEX
24
24
  string :name
25
25
  end
26
-
26
+
27
27
  # These inputs are optional
28
28
  optional do
29
29
  boolean :newsletter_subscribe
30
30
  end
31
-
31
+
32
32
  # The execute method is called only if the inputs validate. It does your business action.
33
33
  def execute
34
34
  user = User.create!(inputs)
@@ -54,7 +54,7 @@ end
54
54
  Some things to note about the example:
55
55
 
56
56
  * We don't need attr_accessible or strong_attributes to protect against mass assignment attacks
57
- * We're guaranteed that within execute, the inputs will be the correct data types, even if they needed some coercion (all strings are stripped by default, and strings like "1" / "0" are converted to true/false for newsletter_subscribe)
57
+ * We're guaranteed that within execute, the inputs will be the correct data types, even if they needed some coercion (all strings are stripped by default, and strings like "1" / "0" are converted to true/false for newsletter_subscribe)
58
58
  * We don't need ActiveRecord validations
59
59
  * We don't need callbacks on our models -- everything is in the execute method (helper methods are also encouraged)
60
60
  * We don't use accepts_nested_attributes_for, even though multiple ActiveRecord models are created
@@ -119,7 +119,7 @@ class CreateComment < Mutations::Command
119
119
  model :article
120
120
  string :comment, max_length: 500
121
121
  end
122
-
122
+
123
123
  def execute; ...; end
124
124
  end
125
125
 
@@ -186,7 +186,7 @@ Your execute method has access to the inputs passed into it:
186
186
  ```ruby
187
187
  self.inputs # white-listed hash of all inputs passed to run. Hash has indifferent access.
188
188
  ```
189
-
189
+
190
190
  If you define an input called _email_, then you'll have these three methods:
191
191
 
192
192
  ```ruby
@@ -5,57 +5,61 @@ module Mutations
5
5
  class: nil, # A constant or string indicates that each element of the array needs to be one of these classes
6
6
  arrayize: false # true will convert "hi" to ["hi"]. "" converts to []
7
7
  }
8
-
8
+
9
9
  def initialize(name, opts = {}, &block)
10
10
  super(opts)
11
-
11
+
12
12
  @name = name
13
13
  @element_filter = nil
14
-
14
+
15
15
  if block_given?
16
16
  instance_eval &block
17
17
  end
18
-
18
+
19
19
  raise ArgumentError.new("Can't supply both a class and a filter") if @element_filter && self.options[:class]
20
20
  end
21
-
21
+
22
22
  def string(options = {})
23
23
  @element_filter = StringFilter.new(options)
24
24
  end
25
-
25
+
26
26
  def integer(options = {})
27
27
  @element_filter = IntegerFilter.new(options)
28
28
  end
29
-
29
+
30
+ def float(options = {})
31
+ @element_filter = FloatFilter.new(options)
32
+ end
33
+
30
34
  def boolean(options = {})
31
35
  @element_filter = BooleanFilter.new(options)
32
36
  end
33
-
37
+
34
38
  def hash(options = {}, &block)
35
39
  @element_filter = HashFilter.new(options, &block)
36
40
  end
37
-
41
+
38
42
  # Advanced types
39
43
  def model(name, options = {})
40
44
  @element_filter = ModelFilter.new(name.to_sym, options)
41
45
  end
42
-
46
+
43
47
  def array(options = {}, &block)
44
48
  @element_filter = ArrayFilter.new(nil, options, &block)
45
49
  end
46
-
50
+
47
51
  def filter(data)
48
52
  # Handle nil case
49
53
  if data.nil?
50
54
  return [nil, nil] if options[:nils]
51
55
  return [nil, :nils]
52
56
  end
53
-
57
+
54
58
  if !data.is_a?(Array) && options[:arrayize]
55
59
  return [[], nil] if data == ""
56
60
  data = Array(data)
57
61
  end
58
-
62
+
59
63
  if data.is_a?(Array)
60
64
  errors = ErrorArray.new
61
65
  filtered_data = []
@@ -63,14 +67,14 @@ module Mutations
63
67
  data.each_with_index do |el, i|
64
68
  el_filtered, el_error = filter_element(el)
65
69
  el_error = ErrorAtom.new(@name, el_error, index: i) if el_error.is_a?(Symbol)
66
-
70
+
67
71
  errors << el_error
68
72
  found_error = true if el_error
69
73
  if !found_error
70
74
  filtered_data << el_filtered
71
75
  end
72
76
  end
73
-
77
+
74
78
  if found_error
75
79
  [data, errors]
76
80
  else
@@ -80,22 +84,22 @@ module Mutations
80
84
  return [data, :array]
81
85
  end
82
86
  end
83
-
87
+
84
88
  # Returns [filtered, errors]
85
89
  def filter_element(data)
86
-
90
+
87
91
  if @element_filter
88
92
  data, el_errors = @element_filter.filter(data)
89
93
  return [data, el_errors] if el_errors
90
94
  elsif options[:class]
91
95
  class_const = options[:class]
92
96
  class_const = class_const.constantize if class_const.is_a?(String)
93
-
97
+
94
98
  if !data.is_a?(class_const)
95
99
  return [data, :class]
96
100
  end
97
101
  end
98
-
102
+
99
103
  [data, nil]
100
104
  end
101
105
  end
@@ -3,23 +3,23 @@ module Mutations
3
3
  @default_options = {
4
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}
8
-
8
+
9
9
  def filter(data)
10
-
10
+
11
11
  # Handle nil case
12
12
  if data.nil?
13
13
  return [nil, nil] if options[:nils]
14
14
  return [nil, :nils]
15
15
  end
16
-
16
+
17
17
  # If data is true or false, we win.
18
18
  return [data, nil] if data == true || data == false
19
-
19
+
20
20
  # If data is a Fixnum, like 1, let's convert it to a string first
21
21
  data = data.to_s if data.is_a?(Fixnum)
22
-
22
+
23
23
  # If data's a string, try to convert it to a boolean. If we can't, it's invalid.
24
24
  if data.is_a?(String)
25
25
  res = BOOL_MAP[data.downcase]
@@ -11,51 +11,51 @@
11
11
 
12
12
  module Mutations
13
13
  class Command
14
-
14
+
15
+ ##
15
16
  ##
16
- ##
17
17
  ##
18
18
  class << self
19
19
  def required(&block)
20
20
  self.input_filters.required(&block)
21
-
21
+
22
22
  self.input_filters.required_keys.each do |key|
23
- define_method(key) do
23
+ define_method(key) do
24
24
  @filtered_input[key]
25
25
  end
26
-
27
- define_method("#{key}_present?") do
26
+
27
+ define_method("#{key}_present?") do
28
28
  @filtered_input.has_key?(key)
29
29
  end
30
-
30
+
31
31
  define_method("#{key}=") do |v|
32
32
  @filtered_input[key] = v
33
33
  end
34
34
  end
35
35
  end
36
-
36
+
37
37
  def optional(&block)
38
38
  self.input_filters.optional(&block)
39
-
39
+
40
40
  self.input_filters.optional_keys.each do |key|
41
- define_method(key) do
41
+ define_method(key) do
42
42
  @filtered_input[key]
43
43
  end
44
-
45
- define_method("#{key}_present?") do
44
+
45
+ define_method("#{key}_present?") do
46
46
  @filtered_input.has_key?(key)
47
47
  end
48
-
48
+
49
49
  define_method("#{key}=") do |v|
50
50
  @filtered_input[key] = v
51
51
  end
52
52
  end
53
53
  end
54
-
54
+
55
55
  def run(*args)
56
56
  new(*args).execute!
57
57
  end
58
-
58
+
59
59
  def run!(*args)
60
60
  m = run(*args)
61
61
  if m.success?
@@ -64,12 +64,12 @@ module Mutations
64
64
  raise ValidationException.new(m.errors)
65
65
  end
66
66
  end
67
-
67
+
68
68
  # Validates input, but doesn't call execute. Returns an Outcome with errors anyway.
69
69
  def validate(*args)
70
70
  new(*args).validation_outcome
71
71
  end
72
-
72
+
73
73
  def input_filters
74
74
  @input_filters ||= begin
75
75
  if Command == self.superclass
@@ -79,9 +79,9 @@ module Mutations
79
79
  end
80
80
  end
81
81
  end
82
-
82
+
83
83
  end
84
-
84
+
85
85
  # Instance methods
86
86
  def initialize(*args)
87
87
  if args.length == 0
@@ -91,24 +91,24 @@ module Mutations
91
91
  raise ArgumentError.new("All arguments must be hashes") unless @original_hash.is_a?(Hash)
92
92
  @original_hash = @original_hash.with_indifferent_access
93
93
  end
94
-
94
+
95
95
  args.each do |a|
96
96
  raise ArgumentError.new("All arguments must be hashes") unless a.is_a?(Hash)
97
97
  @original_hash.merge!(a)
98
98
  end
99
-
99
+
100
100
  @filtered_input, @errors = self.input_filters.filter(@original_hash)
101
101
  end
102
-
102
+
103
103
  def input_filters
104
104
  self.class.input_filters
105
105
  end
106
-
106
+
107
107
  def execute!
108
108
  return Outcome.new(false, nil, @errors) if @errors
109
-
109
+
110
110
  # IDEA/TODO: run validate block
111
-
111
+
112
112
  r = execute
113
113
  if @errors # Execute can add errors
114
114
  return Outcome.new(false, nil, @errors)
@@ -116,7 +116,7 @@ module Mutations
116
116
  return Outcome.new(true, r, nil)
117
117
  end
118
118
  end
119
-
119
+
120
120
  # Runs input thru the filter and sets @filtered_input and @errors
121
121
  def validation_outcome
122
122
  if @errors
@@ -125,14 +125,14 @@ module Mutations
125
125
  Outcome.new(true, nil, nil)
126
126
  end
127
127
  end
128
-
128
+
129
129
  # add_error("name", :too_short)
130
130
  # add_error("colors.foreground", :not_a_color) # => to create errors = {colors: {foreground: :not_a_color}}
131
131
  # or, supply a custom message:
132
132
  # add_error("name", :too_short, "The name 'blahblahblah' is too short!")
133
133
  def add_error(key, kind, message = nil)
134
134
  raise ArgumentError.new("Invalid kind") unless kind.is_a?(Symbol)
135
-
135
+
136
136
  @errors ||= ErrorHash.new
137
137
  cur_errors = @errors
138
138
  parts = key.to_s.split(".")
@@ -147,16 +147,16 @@ module Mutations
147
147
  end
148
148
  @errors
149
149
  end
150
-
150
+
151
151
  def merge_errors(hash)
152
152
  @errors ||= ErrorHash.new
153
153
  @errors.merge!(hash)
154
154
  end
155
-
155
+
156
156
  def inputs
157
157
  @filtered_input
158
158
  end
159
-
159
+
160
160
  def execute
161
161
  # Meant to be overridden
162
162
  end
@@ -1,14 +1,14 @@
1
1
  module Mutations
2
-
2
+
3
3
  # Offers a non-localized, english only, non configurable way to get error messages. This probably isnt good enough for users as-is.
4
4
  class DefaultErrorMessageCreator
5
-
5
+
6
6
  MESSAGES = Hash.new("is invalid").tap do |h|
7
7
  h.merge!(
8
8
  # General
9
9
  nils: "can't be nil",
10
10
  required: "is required",
11
-
11
+
12
12
  # Datatypes
13
13
  string: "isn't a string",
14
14
  integer: "isn't an integer",
@@ -16,26 +16,26 @@ module Mutations
16
16
  hash: "isn't a hash",
17
17
  array: "isn't an array",
18
18
  model: "isn't the right class",
19
-
19
+
20
20
  # String
21
21
  empty: "can't be blank",
22
22
  max_length: "is too long",
23
23
  min_length: "is too short",
24
24
  matches: "isn't in the right format",
25
25
  in: "isn't an option",
26
-
26
+
27
27
  # Array
28
28
  class: "isn't the right class",
29
-
29
+
30
30
  # Integer
31
31
  min: "is too small",
32
32
  max: "is too big",
33
-
33
+
34
34
  # Model
35
35
  new_records: "isn't a saved model"
36
36
  )
37
37
  end
38
-
38
+
39
39
  # key: the name of the field, eg, :email. Could be nil if it's an array element
40
40
  # error_symbol: the validation symbol, eg, :matches or :required
41
41
  # options:
@@ -49,7 +49,7 @@ module Mutations
49
49
  end
50
50
  end
51
51
 
52
-
52
+
53
53
  class ErrorAtom
54
54
 
55
55
  # NOTE: in the future, could also pass in:
@@ -138,7 +138,7 @@ module Mutations
138
138
  list
139
139
  end
140
140
  end
141
-
141
+
142
142
  class ErrorArray < Array
143
143
  def symbolic
144
144
  map {|e| e && e.symbolic }
@@ -1,11 +1,11 @@
1
1
  module Mutations
2
2
  class ValidationException < ::StandardError
3
3
  attr_accessor :errors
4
-
4
+
5
5
  def initialize(errors)
6
6
  self.errors = errors
7
7
  end
8
-
8
+
9
9
  def to_s
10
10
  "#{self.errors.message_list.join('; ')}"
11
11
  end
@@ -3,22 +3,22 @@ module Mutations
3
3
  @default_options = {
4
4
  nils: false, # true allows an explicit nil to be valid. Overrides any other options
5
5
  }
6
-
6
+
7
7
  attr_accessor :optional_inputs
8
8
  attr_accessor :required_inputs
9
-
9
+
10
10
  def initialize(opts = {}, &block)
11
11
  super(opts)
12
-
12
+
13
13
  @optional_inputs = {}
14
14
  @required_inputs = {}
15
15
  @current_inputs = @required_inputs
16
-
16
+
17
17
  if block_given?
18
18
  instance_eval &block
19
19
  end
20
20
  end
21
-
21
+
22
22
  def dup
23
23
  dupped = HashFilter.new
24
24
  @optional_inputs.each_pair do |k, v|
@@ -29,96 +29,100 @@ module Mutations
29
29
  end
30
30
  dupped
31
31
  end
32
-
32
+
33
33
  def required(&block)
34
34
  # TODO: raise if nesting is wrong
35
35
  @current_inputs = @required_inputs
36
36
  instance_eval &block
37
37
  end
38
-
38
+
39
39
  def optional(&block)
40
40
  # TODO: raise if nesting is wrong
41
41
  @current_inputs = @optional_inputs
42
42
  instance_eval &block
43
43
  end
44
-
44
+
45
45
  def required_keys
46
46
  @required_inputs.keys
47
47
  end
48
-
48
+
49
49
  def optional_keys
50
50
  @optional_inputs.keys
51
51
  end
52
-
52
+
53
53
  # Basic types:
54
54
  def string(name, options = {})
55
55
  @current_inputs[name.to_sym] = StringFilter.new(options)
56
56
  end
57
-
57
+
58
58
  def integer(name, options = {})
59
59
  @current_inputs[name.to_sym] = IntegerFilter.new(options)
60
60
  end
61
-
61
+
62
+ def float(name, options = {})
63
+ @current_inputs[name.to_sym] = FloatFilter.new(options)
64
+ end
65
+
62
66
  def boolean(name, options = {})
63
67
  @current_inputs[name.to_sym] = BooleanFilter.new(options)
64
68
  end
65
-
69
+
66
70
  def hash(name, options = {}, &block)
67
71
  @current_inputs[name.to_sym] = HashFilter.new(options, &block)
68
72
  end
69
-
73
+
70
74
  # Advanced types
71
75
  def model(name, options = {})
72
76
  name_sym = name.to_sym
73
77
  @current_inputs[name_sym] = ModelFilter.new(name_sym, options)
74
78
  end
75
-
79
+
76
80
  def array(name, options = {}, &block)
77
81
  name_sym = name.to_sym
78
82
  @current_inputs[name.to_sym] = ArrayFilter.new(name_sym, options, &block)
79
83
  end
80
-
84
+
81
85
  def filter(data)
82
-
86
+
83
87
  # Handle nil case
84
88
  if data.nil?
85
89
  return [nil, nil] if options[:nils]
86
90
  return [nil, :nils]
87
91
  end
88
-
92
+
89
93
  # Ensure it's a hash
90
94
  return [data, :hash] unless data.is_a?(Hash)
91
-
95
+
92
96
  # We always want a hash with indiffernet access
93
97
  unless data.is_a?(HashWithIndifferentAccess)
94
98
  data = data.with_indifferent_access
95
99
  end
96
-
100
+
97
101
  errors = ErrorHash.new
98
102
  filtered_data = HashWithIndifferentAccess.new
99
103
  wildcard_filterer = nil
100
-
104
+
101
105
  [[@required_inputs, true], [@optional_inputs, false]].each do |(inputs, is_required)|
102
106
  inputs.each_pair do |key, filterer|
103
-
107
+
104
108
  # If we are doing wildcards, then record so and move on
105
109
  if key == :*
106
110
  wildcard_filterer = filterer
107
111
  next
108
112
  end
109
-
113
+
110
114
  data_element = data[key]
111
-
115
+
112
116
  # First, discard optional nils/empty params
113
117
  data.delete(key) if !is_required && data.has_key?(key) && filterer.discard_nils? && data_element.nil?
114
118
  data.delete(key) if !is_required && data.has_key?(key) && filterer.discard_empty? && data_element == ""
115
-
119
+
116
120
  default_used = false
117
121
  if !data.has_key?(key) && filterer.has_default?
118
122
  data_element = filterer.default
119
123
  default_used = true
120
124
  end
121
-
125
+
122
126
  if data.has_key?(key) || default_used
123
127
  sub_data, sub_error = filterer.filter(data_element)
124
128
 
@@ -133,17 +137,17 @@ module Mutations
133
137
  end
134
138
  end
135
139
  end
136
-
140
+
137
141
  if wildcard_filterer
138
142
  filtered_keys = data.keys - filtered_data.keys
139
-
143
+
140
144
  filtered_keys.each do |key|
141
145
  data_element = data[key]
142
-
146
+
143
147
  # First, discard optional nils/empty params
144
148
  next if data.has_key?(key) && wildcard_filterer.discard_nils? && data_element.nil?
145
149
  next if data.has_key?(key) && wildcard_filterer.discard_empty? && data_element == ""
146
-
150
+
147
151
  sub_data, sub_error = wildcard_filterer.filter(data_element)
148
152
  if sub_error.nil?
149
153
  filtered_data[key] = sub_data
@@ -153,7 +157,7 @@ module Mutations
153
157
  end
154
158
  end
155
159
  end
156
-
160
+
157
161
  if errors.any?
158
162
  [data, errors]
159
163
  else
@@ -1,36 +1,36 @@
1
1
  module Mutations
2
2
  class InputFilter
3
3
  @default_options = {}
4
-
4
+
5
5
  def self.default_options
6
6
  @default_options
7
7
  end
8
-
8
+
9
9
  attr_accessor :options
10
-
10
+
11
11
  def initialize(opts = {})
12
12
  self.options = (self.class.default_options || {}).merge(opts)
13
13
  end
14
-
14
+
15
15
  # returns -> [sanitized data, error]
16
16
  # If an error is returned, then data will be nil
17
17
  def filter(data)
18
18
  [data, nil]
19
19
  end
20
-
20
+
21
21
  def has_default?
22
22
  options.has_key?(:default)
23
23
  end
24
-
24
+
25
25
  def default
26
26
  options[:default]
27
27
  end
28
-
28
+
29
29
  # Only relevant for optional params
30
30
  def discard_nils?
31
31
  !options[:nils]
32
32
  end
33
-
33
+
34
34
  def discard_empty?
35
35
  options[:discard_empty]
36
36
  end