mutations 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,4 @@
1
1
  .rvmrc
2
+ .ruby-version
2
3
  *.gem
4
+ Gemfile.lock
data/.travis.yml CHANGED
@@ -1,10 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
3
  - 1.9.3
5
4
  - jruby-19mode
6
- - rbx-19mode
5
+ - rbx
7
6
  - 2.0.0
8
- branches:
9
- only:
10
- - master
7
+ - 2.1.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.7.0
2
+ -----------
3
+
4
+ - Ruby 2.1 support added.
5
+ - Ruby 1.8.7 support removed.
6
+ - Rubinius support updated.
7
+ - Gemfile.lock removed (Rails 4 support, etc)
8
+ - API change: Add ability to implement a 'validate' method
9
+ - ```discard_invalid``` option added
10
+ - AdditionFilters: Gain ability to pass blocks to filters.
11
+
1
12
  0.6.0
2
13
  -----------
3
14
 
data/Gemfile CHANGED
@@ -3,6 +3,12 @@ gemspec
3
3
 
4
4
  gem 'activesupport'
5
5
 
6
+ platforms :rbx do
7
+ gem 'rubysl', '~> 2.0'
8
+ gem 'psych'
9
+ gem 'rubinius-developer_tools'
10
+ end
11
+
6
12
  group :test do
7
13
  gem 'minitest', '~> 4.0'
8
14
  end
data/README.md CHANGED
@@ -233,7 +233,23 @@ unless outcome.success?
233
233
  end
234
234
  ```
235
235
 
236
- You can add errors within execute if the default validations are insufficient:
236
+ You can add errors in a validate method if the default validations are insufficient. Errors added by validate will prevent the execute method from running.
237
+
238
+ ```ruby
239
+ #...
240
+ def validate
241
+ if password != password_confirmation
242
+ add_error(:password_confirmation, :doesnt_match, "Your passwords don't match")
243
+ end
244
+ end
245
+ # ...
246
+
247
+ # That error would show up in the errors hash:
248
+ outcome.errors.symbolic # => {password_confirmation: :doesnt_match}
249
+ outcome.errors.message # => {password_confirmation: "Your passwords don't match"}
250
+ ```
251
+
252
+ Alternatively you can also add these validations in the execute method:
237
253
 
238
254
  ```ruby
239
255
  #...
@@ -271,4 +287,3 @@ Yes, but I don't think it's a very good idea. Better to compose.
271
287
  ### Can I use this with Rails forms helpers?
272
288
 
273
289
  Somewhat. Any form can submit to your server, and mutations will happily accept that input. However, if there are errors, there's no built-in way to bake the errors into the HTML with Rails form tag helpers. Right now this is really designed to support a JSON API. You'd probably have to write an adapter of some kind.
274
-
data/lib/mutations.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'active_support'
2
1
  require 'active_support/core_ext/hash/indifferent_access'
3
2
  require 'active_support/core_ext/string/inflections'
4
3
  require 'date'
@@ -30,11 +29,11 @@ module Mutations
30
29
  def error_message_creator=(creator)
31
30
  @error_message_creator = creator
32
31
  end
33
-
32
+
34
33
  def cache_constants=(val)
35
34
  @cache_constants = val
36
35
  end
37
-
36
+
38
37
  def cache_constants?
39
38
  @cache_constants
40
39
  end
@@ -4,7 +4,7 @@ require 'mutations/array_filter'
4
4
  module Mutations
5
5
  class AdditionalFilter < InputFilter
6
6
  def self.inherited(subclass)
7
- type_name = subclass.name[/^Mutations::([a-zA-Z]*)Filter$/, 1].downcase
7
+ type_name = subclass.name[/^Mutations::([a-zA-Z]*)Filter$/, 1].underscore
8
8
 
9
9
  Mutations::HashFilter.register_additional_filter(subclass, type_name)
10
10
  Mutations::ArrayFilter.register_additional_filter(subclass, type_name)
@@ -57,15 +57,15 @@ module Mutations
57
57
  data.each_with_index do |el, i|
58
58
  el_filtered, el_error = filter_element(el)
59
59
  el_error = ErrorAtom.new(@name, el_error, :index => i) if el_error.is_a?(Symbol)
60
-
61
60
  errors << el_error
62
- found_error = true if el_error
63
- if !found_error
61
+ if el_error
62
+ found_error = true
63
+ else
64
64
  filtered_data << el_filtered
65
65
  end
66
66
  end
67
67
 
68
- if found_error
68
+ if found_error && !(@element_filter && @element_filter.discard_invalid?)
69
69
  [data, errors]
70
70
  else
71
71
  [filtered_data, nil]
@@ -38,7 +38,7 @@ module Mutations
38
38
 
39
39
  # Validates input, but doesn't call execute. Returns an Outcome with errors anyway.
40
40
  def validate(*args)
41
- new(*args).validate
41
+ new(*args).validation_outcome
42
42
  end
43
43
 
44
44
  def input_filters
@@ -60,7 +60,11 @@ module Mutations
60
60
  h.merge!(arg)
61
61
  end
62
62
 
63
+ # Do field-level validation / filtering:
63
64
  @inputs, @errors = self.input_filters.filter(@raw_inputs)
65
+
66
+ # Run a custom validation method if supplied:
67
+ validate unless has_errors?
64
68
  end
65
69
 
66
70
  def input_filters
@@ -85,10 +89,6 @@ module Mutations
85
89
  end
86
90
  end
87
91
 
88
- def validate
89
- validation_outcome
90
- end
91
-
92
92
  def validation_outcome(result = nil)
93
93
  Outcome.new(!has_errors?, has_errors? ? nil : result, @errors, @inputs)
94
94
  end
@@ -97,6 +97,10 @@ module Mutations
97
97
 
98
98
  attr_reader :inputs, :raw_inputs
99
99
 
100
+ def validate
101
+ # Meant to be overridden
102
+ end
103
+
100
104
  def execute
101
105
  # Meant to be overridden
102
106
  end
@@ -112,7 +116,7 @@ module Mutations
112
116
  @errors.tap do |errs|
113
117
  path = key.to_s.split(".")
114
118
  last = path.pop
115
- inner = path.inject(errs) do |cut_errors,part|
119
+ inner = path.inject(errs) do |cur_errors,part|
116
120
  cur_errors[part.to_sym] ||= ErrorHash.new
117
121
  end
118
122
  inner[last] = ErrorAtom.new(key, kind, :message => message)
@@ -120,8 +124,10 @@ module Mutations
120
124
  end
121
125
 
122
126
  def merge_errors(hash)
123
- @errors ||= ErrorHash.new
124
- @errors.merge!(hash)
127
+ if hash.any?
128
+ @errors ||= ErrorHash.new
129
+ @errors.merge!(hash)
130
+ end
125
131
  end
126
132
 
127
133
  end
@@ -1,11 +1,11 @@
1
1
  module Mutations
2
2
  class HashFilter < InputFilter
3
3
  def self.register_additional_filter(type_class, type_name)
4
- define_method(type_name) do | *args |
4
+ define_method(type_name) do | *args, &block |
5
5
  name = args[0]
6
6
  options = args[1] || {}
7
7
 
8
- @current_inputs[name.to_sym] = type_class.new(options)
8
+ @current_inputs[name.to_sym] = type_class.new(options, &block)
9
9
  end
10
10
  end
11
11
 
@@ -103,7 +103,7 @@ module Mutations
103
103
 
104
104
  # First, discard optional nils/empty params
105
105
  data.delete(key) if !is_required && data.has_key?(key) && filterer.discard_nils? && data_element.nil?
106
- data.delete(key) if !is_required && data.has_key?(key) && filterer.discard_empty? && data_element == ""
106
+ data.delete(key) if !is_required && data.has_key?(key) && filterer.discard_empty? && data_element == "" # BUG: this doesn't account for data_elem being " "
107
107
 
108
108
  default_used = false
109
109
  if !data.has_key?(key) && filterer.has_default?
@@ -116,6 +116,8 @@ module Mutations
116
116
 
117
117
  if sub_error.nil?
118
118
  filtered_data[key] = sub_data
119
+ elsif !is_required && filterer.discard_invalid?
120
+ data.delete(key)
119
121
  else
120
122
  sub_error = ErrorAtom.new(key, sub_error) if sub_error.is_a?(Symbol)
121
123
  errors[key] = sub_error
@@ -139,6 +141,8 @@ module Mutations
139
141
  sub_data, sub_error = wildcard_filterer.filter(data_element)
140
142
  if sub_error.nil?
141
143
  filtered_data[key] = sub_data
144
+ elsif wildcard_filterer.discard_invalid?
145
+ data.delete(key)
142
146
  else
143
147
  sub_error = ErrorAtom.new(key, sub_error) if sub_error.is_a?(Symbol)
144
148
  errors[key] = sub_error
@@ -34,5 +34,9 @@ module Mutations
34
34
  def discard_empty?
35
35
  options[:discard_empty]
36
36
  end
37
+
38
+ def discard_invalid?
39
+ self.options[:discard_invalid]
40
+ end
37
41
  end
38
42
  end
@@ -2,7 +2,7 @@ module Mutations
2
2
  class StringFilter < AdditionalFilter
3
3
  @default_options = {
4
4
  :strip => true, # true calls data.strip if data is a string
5
- :strict => false, # If false, then symbols, numbers, and booleans are converted to a string with to_s. # TODO: TEST
5
+ :strict => false, # If false, then symbols, numbers, and booleans are converted to a string with to_s.
6
6
  :nils => false, # true allows an explicit nil to be valid. Overrides any other options
7
7
  :empty => false, # false disallows "". true allows "" and overrides any other validations (b/c they couldn't be true if it's empty)
8
8
  :min_length => nil, # Can be a number like 5, meaning that 5 codepoints are required
@@ -1,3 +1,3 @@
1
1
  module Mutations
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/mutations.gemspec CHANGED
@@ -15,4 +15,4 @@ Gem::Specification.new do |s|
15
15
  s.add_dependency 'activesupport'
16
16
  s.add_development_dependency 'minitest', '~> 4'
17
17
  s.add_development_dependency 'rake'
18
- end
18
+ end
@@ -13,20 +13,31 @@ describe "Mutations::AdditionalFilter" do
13
13
  return [data, nil]
14
14
  end
15
15
  end
16
+
17
+ class MultiWordTestFilter < Mutations::AdditionalFilter
18
+ @default_options = {
19
+ :nils => false
20
+ }
21
+
22
+ def filter(data)
23
+ return [data, nil]
24
+ end
25
+ end
16
26
  end
17
27
 
18
28
  class TestCommandUsingAdditionalFilters < Mutations::Command
19
29
  required do
20
30
  sometest :first_name
31
+ multi_word_test :last_name
21
32
  end
22
33
 
23
34
  def execute
24
- { :first_name => first_name }
35
+ { :first_name => first_name, :last_name => last_name }
25
36
  end
26
37
  end
27
38
 
28
39
  it "should recognize additional filters" do
29
- outcome = TestCommandUsingAdditionalFilters.run(:first_name => "John")
40
+ outcome = TestCommandUsingAdditionalFilters.run(:first_name => "John", :last_name => "Doe")
30
41
  assert outcome.success?
31
42
  assert_equal nil, outcome.errors
32
43
  end
@@ -45,7 +56,7 @@ describe "Mutations::AdditionalFilter" do
45
56
 
46
57
  it "should be useable in hashes" do
47
58
  outcome = TestCommandUsingAdditionalFiltersInHashes.run(
48
- :a_hash => { :first_name => "John" }
59
+ :a_hash => { :first_name => "John" }
49
60
  )
50
61
 
51
62
  assert outcome.success?
@@ -72,5 +83,47 @@ describe "Mutations::AdditionalFilter" do
72
83
  assert outcome.success?
73
84
  assert_equal nil, outcome.errors
74
85
  end
86
+
87
+ module Mutations
88
+ class AdditionalWithBlockFilter < Mutations::AdditionalFilter
89
+
90
+ def initialize(opts={}, &block)
91
+ super(opts)
92
+
93
+ if block_given?
94
+ instance_eval &block
95
+ end
96
+ end
97
+
98
+ def should_be_called
99
+ @was_called = true
100
+ end
101
+
102
+ def filter(data)
103
+ if @was_called
104
+ [true, nil]
105
+ else
106
+ [nil, :not_called]
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ class TestCommandUsingBlockArgument < Mutations::Command
113
+ required do
114
+ additional_with_block :foo do
115
+ should_be_called
116
+ end
117
+ end
118
+
119
+ def execute
120
+ true
121
+ end
122
+ end
123
+
124
+ it "can have a block constructor" do
125
+ assert_equal true, TestCommandUsingBlockArgument.run!(:foo => 'bar')
126
+ end
127
+
75
128
  end
76
129
  end
@@ -181,4 +181,12 @@ describe "Mutations::ArrayFilter" do
181
181
  assert_equal ["Array[2] isn't a string", "Array[0] can't be blank"], errors.message_list
182
182
  end
183
183
 
184
+ it "strips invalid elements" do
185
+ f = Mutations::ArrayFilter.new(:arr) do
186
+ integer :discard_invalid => true
187
+ end
188
+ filtered, errors = f.filter([1, "2", "three", "4", 5, [6]])
189
+ assert_equal [1,2,4,5], filtered
190
+ assert_equal nil, errors
191
+ end
184
192
  end
data/spec/command_spec.rb CHANGED
@@ -50,6 +50,29 @@ describe "Command" do
50
50
  assert_equal :max_length, outcome.errors.symbolic[:name]
51
51
  end
52
52
 
53
+ it "should execute a custom validate method" do
54
+ outcome = SimpleCommand.validate(:name => "JohnLong", :email => "xxxx")
55
+
56
+ assert !outcome.success?
57
+ assert_equal :invalid, outcome.errors.symbolic[:email]
58
+ end
59
+
60
+ it "should execute custom validate method during run" do
61
+ outcome = SimpleCommand.run(:name => "JohnLong", :email => "xxxx")
62
+
63
+ assert !outcome.success?
64
+ assert_nil outcome.result
65
+ assert_equal :invalid, outcome.errors.symbolic[:email]
66
+ end
67
+
68
+ it "should execute custom validate method only if regular validations succeed" do
69
+ outcome = SimpleCommand.validate(:name => "JohnTooLong", :email => "xxxx")
70
+
71
+ assert !outcome.success?
72
+ assert_equal :max_length, outcome.errors.symbolic[:name]
73
+ assert_equal nil, outcome.errors.symbolic[:email]
74
+ end
75
+
53
76
  it "should merge multiple hashes" do
54
77
  outcome = SimpleCommand.run({:name => "John", :email => "john@gmail.com"}, {:email => "bob@jones.com", :amount => 5})
55
78
 
@@ -145,6 +168,28 @@ describe "Command" do
145
168
  end
146
169
  end
147
170
 
171
+ describe "NestingErrorfulCommand" do
172
+ class NestingErrorfulCommand < Mutations::Command
173
+
174
+ required { string :name }
175
+ optional { string :email }
176
+
177
+ def execute
178
+ add_error("people.bob", :is_a_bob)
179
+
180
+ 1
181
+ end
182
+ end
183
+
184
+ it "should let you add errors nested under a namespace" do
185
+ outcome = NestingErrorfulCommand.run(:name => "John", :email => "john@gmail.com")
186
+
187
+ assert !outcome.success?
188
+ assert_nil outcome.result
189
+ assert :is_a_bob, outcome.errors[:people].symbolic[:bob]
190
+ end
191
+ end
192
+
148
193
  describe "MultiErrorCommand" do
149
194
  class ErrorfulCommand < Mutations::Command
150
195
 
@@ -186,4 +186,48 @@ describe "Mutations::HashFilter" do
186
186
  end
187
187
  end
188
188
 
189
+ describe "discarding invalid values" do
190
+ it "should discard invalid optional values" do
191
+ hf = Mutations::HashFilter.new do
192
+ required do
193
+ string :foo
194
+ end
195
+ optional do
196
+ integer :bar, :discard_invalid => true
197
+ end
198
+ end
199
+
200
+ filtered, errors = hf.filter(:foo => "bar", :bar => "baz")
201
+ assert_equal ({"foo" => "bar"}), filtered
202
+ assert_equal nil, errors
203
+ end
204
+
205
+ it "should discard invalid optional values for wildcards" do
206
+ hf = Mutations::HashFilter.new do
207
+ required do
208
+ string :foo
209
+ end
210
+ optional do
211
+ integer :*, :discard_invalid => true
212
+ end
213
+ end
214
+
215
+ filtered, errors = hf.filter(:foo => "bar", :bar => "baz", :wat => 1)
216
+ assert_equal ({"foo" => "bar", "wat" => 1}), filtered
217
+ assert_equal nil, errors
218
+ end
219
+
220
+
221
+ it "should not discard invalid require values" do
222
+ hf = Mutations::HashFilter.new do
223
+ required do
224
+ integer :foo, :discard_invalid => true
225
+ end
226
+ end
227
+
228
+ filtered, errors = hf.filter(:foo => "bar")
229
+ assert_equal ({"foo" => :integer}), errors.symbolic
230
+ end
231
+ end
232
+
189
233
  end
@@ -9,7 +9,14 @@ class SimpleCommand < Mutations::Command
9
9
  integer :amount
10
10
  end
11
11
 
12
+ def validate
13
+ unless email && email.include?('@')
14
+ add_error(:email, :invalid, 'Email must contain @')
15
+ end
16
+ end
17
+
12
18
  def execute
13
19
  inputs
14
20
  end
21
+
15
22
  end
@@ -135,4 +135,46 @@ describe "Mutations::StringFilter" do
135
135
  assert_equal "red", filtered
136
136
  assert_equal nil, errors
137
137
  end
138
+
139
+ it "converts symbols to strings" do
140
+ sf = Mutations::StringFilter.new(:strict => false)
141
+ filtered, errors = sf.filter(:my_sym)
142
+ assert_equal "my_sym", filtered
143
+ assert_equal nil, errors
144
+ end
145
+
146
+ it "converts integers to strings" do
147
+ sf = Mutations::StringFilter.new(:strict => false)
148
+ filtered, errors = sf.filter(1)
149
+ assert_equal "1", filtered
150
+ assert_equal nil, errors
151
+ end
152
+
153
+ it "converts booleans to strings" do
154
+ sf = Mutations::StringFilter.new(:strict => false)
155
+ filtered, errors = sf.filter(true)
156
+ assert_equal "true", filtered
157
+ assert_equal nil, errors
158
+ end
159
+
160
+ it "disallows symbols" do
161
+ sf = Mutations::StringFilter.new(:strict => true)
162
+ filtered, errors = sf.filter(:my_sym)
163
+ assert_equal :my_sym, filtered
164
+ assert_equal :string, errors
165
+ end
166
+
167
+ it "disallows integers" do
168
+ sf = Mutations::StringFilter.new(:strict => true)
169
+ filtered, errors = sf.filter(1)
170
+ assert_equal 1, filtered
171
+ assert_equal :string, errors
172
+ end
173
+
174
+ it "disallows booleans" do
175
+ sf = Mutations::StringFilter.new(:strict => true)
176
+ filtered, errors = sf.filter(true)
177
+ assert_equal true, filtered
178
+ assert_equal :string, errors
179
+ end
138
180
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-22 00:00:00.000000000 Z
12
+ date: 2013-12-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -70,7 +70,6 @@ files:
70
70
  - .travis.yml
71
71
  - CHANGELOG.md
72
72
  - Gemfile
73
- - Gemfile.lock
74
73
  - MIT-LICENSE
75
74
  - README.md
76
75
  - Rakefile
data/Gemfile.lock DELETED
@@ -1,25 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- mutations (0.5.12)
5
- activesupport
6
-
7
- GEM
8
- remote: http://rubygems.org/
9
- specs:
10
- activesupport (3.2.8)
11
- i18n (~> 0.6)
12
- multi_json (~> 1.0)
13
- i18n (0.6.1)
14
- minitest (4.1.0)
15
- multi_json (1.3.6)
16
- rake (0.9.2.2)
17
-
18
- PLATFORMS
19
- ruby
20
-
21
- DEPENDENCIES
22
- activesupport
23
- minitest (~> 4.0)
24
- mutations!
25
- rake