mutations 0.5.9 → 0.5.10

Sign up to get free protection for your applications and to get access to all the features.
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