Skip to content

Commit d734415

Browse files
dgasperdblock
authored andcommitted
Support fail_fast param validation option (#1499)
1 parent 5683154 commit d734415

File tree

9 files changed

+87
-15
lines changed

9 files changed

+87
-15
lines changed

.rubocop_todo.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2016-09-11 17:59:25 +0900 using RuboCop version 0.39.0.
3+
# on 2016-09-28 13:52:41 +0200 using RuboCop version 0.39.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 42
9+
# Offense count: 41
1010
Metrics/AbcSize:
1111
Max: 44
1212

1313
# Offense count: 1
1414
Metrics/BlockNesting:
1515
Max: 4
1616

17-
# Offense count: 7
17+
# Offense count: 8
1818
# Configuration parameters: CountComments.
1919
Metrics/ClassLength:
20-
Max: 277
20+
Max: 279
2121

2222
# Offense count: 28
2323
Metrics/CyclomaticComplexity:
2424
Max: 14
2525

26-
# Offense count: 933
26+
# Offense count: 955
2727
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes.
2828
# URISchemes: http, https
2929
Metrics/LineLength:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [#1486](https://github.com/ruby-grape/grape/pull/1486): Implemented except in values validator - [@jonmchan](https://github.com/jonmchan).
66
* [#1470](https://github.com/ruby-grape/grape/pull/1470): Drop support for ruby-2.0 - [@namusyaka](https://github.com/namusyaka).
77
* [#1490](https://github.com/ruby-grape/grape/pull/1490): Switch to Ruby-2.x+ syntax - [@namusyaka](https://github.com/namusyaka).
8+
* [#1499](https://github.com/ruby-grape/grape/pull/1499): Support fail_fast param validation option - [@dgasper](https://github.com/dgasper).
89
* Your contribution here.
910

1011
#### Fixes

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,25 @@ subject.rescue_from Grape::Exceptions::ValidationErrors do |e|
13551355
end
13561356
```
13571357

1358+
Grape returns all validation and coercion errors found by default.
1359+
To skip all subsequent validation checks when a specific param is found invalid, use `fail_fast: true`.
1360+
1361+
The following example will not check if `:wine` is present unless it finds `:beer`.
1362+
```ruby
1363+
params do
1364+
required :beer, fail_fast: true
1365+
required :wine
1366+
end
1367+
```
1368+
The result of empty params would be a single `Grape::Exceptions::ValidationErrors` error.
1369+
1370+
Similarly, no regular expression test will be performed if `:blah` is blank in the following example.
1371+
```ruby
1372+
params do
1373+
required :blah, allow_blank: false, regexp: /blah/, fail_fast: true
1374+
end
1375+
```
1376+
13581377
### I18n
13591378

13601379
Grape supports I18n for parameter-related error messages, but will fallback to English if

lib/grape/endpoint.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,10 @@ def run_validators(validators, request)
343343
validator.validate(request)
344344
rescue Grape::Exceptions::Validation => e
345345
validation_errors << e
346+
break if validator.fail_fast?
346347
rescue Grape::Exceptions::ValidationArrayErrors => e
347348
validation_errors += e.errors
349+
break if validator.fail_fast?
348350
end
349351
end
350352

lib/grape/validations/params_scope.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,20 +230,24 @@ def validates(attrs, validations)
230230
full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
231231
@api.document_attribute(full_attrs, doc_attrs)
232232

233+
# slice out fail_fast attribute
234+
opts = {}
235+
opts[:fail_fast] = validations.delete(:fail_fast) || false
236+
233237
# Validate for presence before any other validators
234238
if validations.key?(:presence) && validations[:presence]
235-
validate('presence', validations[:presence], attrs, doc_attrs)
239+
validate('presence', validations[:presence], attrs, doc_attrs, opts)
236240
validations.delete(:presence)
237241
validations.delete(:message) if validations.key?(:message)
238242
end
239243

240244
# Before we run the rest of the validators, let's handle
241245
# whatever coercion so that we are working with correctly
242246
# type casted values
243-
coerce_type validations, attrs, doc_attrs
247+
coerce_type validations, attrs, doc_attrs, opts
244248

245249
validations.each do |type, options|
246-
validate(type, options, attrs, doc_attrs)
250+
validate(type, options, attrs, doc_attrs, opts)
247251
end
248252
end
249253

@@ -308,7 +312,7 @@ def check_coerce_with(validations)
308312
# composited from more than one +requires+/+optional+
309313
# parameter, and needs to be run before most other
310314
# validations.
311-
def coerce_type(validations, attrs, doc_attrs)
315+
def coerce_type(validations, attrs, doc_attrs, opts)
312316
check_coerce_with(validations)
313317

314318
return unless validations.key?(:coerce)
@@ -318,7 +322,7 @@ def coerce_type(validations, attrs, doc_attrs)
318322
method: validations[:coerce_with],
319323
message: validations[:coerce_message]
320324
}
321-
validate('coerce', coerce_options, attrs, doc_attrs)
325+
validate('coerce', coerce_options, attrs, doc_attrs, opts)
322326
validations.delete(:coerce_with)
323327
validations.delete(:coerce)
324328
validations.delete(:coerce_message)
@@ -337,12 +341,12 @@ def check_incompatible_option_values(values, default)
337341
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values)
338342
end
339343

340-
def validate(type, options, attrs, doc_attrs)
344+
def validate(type, options, attrs, doc_attrs, opts)
341345
validator_class = Validations.validators[type.to_s]
342346

343347
raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
344348

345-
value = validator_class.new(attrs, options, doc_attrs[:required], self)
349+
value = validator_class.new(attrs, options, doc_attrs[:required], self, opts)
346350
@api.namespace_stackable(:validations, value)
347351
end
348352

lib/grape/validations/validators/base.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ class Base
1010
# @param options [Object] implementation-dependent Validator options
1111
# @param required [Boolean] attribute(s) are required or optional
1212
# @param scope [ParamsScope] parent scope for this Validator
13-
def initialize(attrs, options, required, scope)
13+
# @param opts [Hash] additional validation options
14+
def initialize(attrs, options, required, scope, opts = {})
1415
@attrs = Array(attrs)
1516
@option = options
1617
@required = required
1718
@scope = scope
19+
@fail_fast = opts[:fail_fast] || false
1820
end
1921

2022
# Validates a given request.
@@ -76,6 +78,10 @@ def options_key?(key, options = nil)
7678
options = instance_variable_get(:@option) if options.nil?
7779
options.respond_to?(:key?) && options.key?(key) && !options[key].nil?
7880
end
81+
82+
def fail_fast?
83+
@fail_fast
84+
end
7985
end
8086
end
8187
end

lib/grape/validations/validators/default.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Grape
22
module Validations
33
class DefaultValidator < Base
4-
def initialize(attrs, options, required, scope)
4+
def initialize(attrs, options, required, scope, opts = {})
55
@default = options
66
super
77
end

lib/grape/validations/validators/values.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Grape
22
module Validations
33
class ValuesValidator < Base
4-
def initialize(attrs, options, required, scope)
4+
def initialize(attrs, options, required, scope, opts = {})
55
@excepts = (options_key?(:except, options) ? options[:except] : [])
66
@values = (options_key?(:value, options) ? options[:value] : [])
77

spec/grape/validations/params_scope_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,4 +584,44 @@ def initialize(value)
584584
get '/nested', bar: { a: 'x', c: { b: 'yes' } }
585585
expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'c' => { 'b' => 'yes' } })
586586
end
587+
588+
context 'failing fast' do
589+
context 'when fail_fast is not defined' do
590+
it 'does not stop validation' do
591+
subject.params do
592+
requires :one
593+
requires :two
594+
requires :three
595+
end
596+
subject.get('/fail-fast') { declared(params).to_json }
597+
598+
get '/fail-fast'
599+
expect(last_response.status).to eq(400)
600+
expect(last_response.body).to eq('one is missing, two is missing, three is missing')
601+
end
602+
end
603+
context 'when fail_fast is defined it stops the validation' do
604+
it 'of other params' do
605+
subject.params do
606+
requires :one, fail_fast: true
607+
requires :two
608+
end
609+
subject.get('/fail-fast') { declared(params).to_json }
610+
611+
get '/fail-fast'
612+
expect(last_response.status).to eq(400)
613+
expect(last_response.body).to eq('one is missing')
614+
end
615+
it 'for a single param' do
616+
subject.params do
617+
requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true
618+
end
619+
subject.get('/fail-fast') { declared(params).to_json }
620+
621+
get '/fail-fast', one: ''
622+
expect(last_response.status).to eq(400)
623+
expect(last_response.body).to eq('one is empty')
624+
end
625+
end
626+
end
587627
end

0 commit comments

Comments
 (0)