Skip to content

Commit 5ec9c57

Browse files
committed
Fix: ruby-grape#211: Custom validator shouldn't be triggered when optional and not present
Updated validator to only run custom optional validation when the param is present. Re-wrote validations_spec to be a little more robust. Added section to documentation for custom validation.
1 parent 261b40d commit 5ec9c57

File tree

4 files changed

+125
-32
lines changed

4 files changed

+125
-32
lines changed

CHANGELOG.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Next Release
22
============
33

4+
* [#211](https://github.com/intridea/grape/pull/211): Fix: Custom param validator shouldn't validate when optional and param not present - [@adamgotterer](https://github.com/adamgotterer).
45
* [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer).
56
* [#201](https://github.com/intridea/grape/pull/201): Rewritten `params` DSL, including support for coercion and validations - [@schmurfy](https://github.com/schmurfy).
67
* [#205](https://github.com/intridea/grape/pull/205): Fix: Corrected parsing of empty JSON body on POST/PUT - [@tim-vandecasteele](https://github.com/tim-vandecasteele).

README.markdown

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,13 @@ get ':id' do
226226
end
227227
```
228228

229+
When a type is specified an implicit validation is done after the coercion to ensure
230+
the output type is the one declared.
231+
232+
### Namespace Validation and Coercion
229233
Namespaces allow parameter definitions and apply to every method within the namespace.
230234

231-
``` ruby
235+
```ruby
232236
namespace :shelves do
233237
params do
234238
requires :shelf_id, type: Integer, desc: "A shelf."
@@ -246,8 +250,41 @@ namespace :shelves do
246250
end
247251
```
248252

249-
When a type is specified an implicit validation is done after the coercion to ensure
250-
the output type is the one declared.
253+
### Custom Validators
254+
```ruby
255+
class doit < Grape::Validations::Validator
256+
def validate_param!(attr_name, params)
257+
unless params[attr_name] == 'im custom'
258+
throw :error, :status => 400, :message => "#{attr_name}: is not custom!"
259+
end
260+
end
261+
end
262+
```
263+
264+
```ruby
265+
params do
266+
requires :name, :doit => true
267+
end
268+
```
269+
270+
You can also create custom classes that take additional parameters
271+
```ruby
272+
class Length < Grape::Validations::SingleOptionValidator
273+
def validate_param!(attr_name, params)
274+
unless params[attr_name].length == @option
275+
throw :error, :status => 400, :message => "#{attr_name}: must be #{@option} characters long"
276+
end
277+
end
278+
end
279+
```
280+
281+
```ruby
282+
params do
283+
requires :name, :length => 5
284+
end
285+
```
286+
287+
251288

252289
## Headers
253290

lib/grape/validations.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ module Validations
88
# All validators must inherit from this class.
99
#
1010
class Validator
11-
def initialize(attrs, options)
11+
def initialize(attrs, options, doc_attrs)
1212
@attrs = Array(attrs)
13+
@doc_attrs = doc_attrs
1314

1415
if options.is_a?(Hash) && !options.empty?
1516
raise "unknown options: #{options.keys}"
@@ -18,7 +19,9 @@ def initialize(attrs, options)
1819

1920
def validate!(params)
2021
@attrs.each do |attr_name|
21-
validate_param!(attr_name, params)
22+
if @doc_attrs[:required] || params.has_key?(attr_name)
23+
validate_param!(attr_name, params)
24+
end
2225
end
2326
end
2427

@@ -37,7 +40,7 @@ def self.convert_to_short_name(klass)
3740
##
3841
# Base class for all validators taking only one param.
3942
class SingleOptionValidator < Validator
40-
def initialize(attrs, options)
43+
def initialize(attrs, options, doc_attrs)
4144
@option = options
4245
super
4346
end
@@ -109,7 +112,7 @@ def validates(attrs, validations)
109112
validations.each do |type, options|
110113
validator_class = Validations::validators[type.to_s]
111114
if validator_class
112-
@api.settings[:validations] << validator_class.new(attrs, options)
115+
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs)
113116
else
114117
raise "unknown validator: #{type}"
115118
end

spec/grape/validations_spec.rb

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,85 @@
11
require 'spec_helper'
22

3-
describe Grape::Validations do
4-
module ValidationsSpec
5-
class API < Grape::API
6-
default_format :json
7-
8-
params do
9-
requires :name, :company
10-
optional :a_number, :regexp => /^[0-9]+$/
3+
module CustomValidations
4+
class Customvalidator < Grape::Validations::Validator
5+
def validate_param!(attr_name, params)
6+
unless params[attr_name] == 'im custom'
7+
throw :error, :status => 400, :message => "#{attr_name}: is not custom!"
8+
end
9+
end
10+
end
11+
end
12+
13+
describe Grape::API do
14+
subject { Class.new(Grape::API) }
15+
def app; subject end
16+
17+
describe 'params' do
18+
it 'validates optional parameter if present' do
19+
subject.params { optional :a_number, :regexp => /^[0-9]+$/ }
20+
subject.get '/optional' do 'optional works!'; end
21+
22+
get '/optional', { :a_number => 'string' }
23+
last_response.status.should == 400
24+
last_response.body.should == 'invalid parameter: a_number'
25+
26+
get '/optional', { :a_number => 45 }
27+
last_response.status.should == 200
28+
last_response.body.should == 'optional works!'
29+
end
30+
31+
context 'when using optional with a custom validator' do
32+
before do
33+
subject.params { optional :custom, :customvalidator => true }
34+
subject.get '/optional_custom' do 'optional with custom works!'; end
1135
end
12-
13-
get do
14-
"Hello"
36+
37+
it 'validates when param is present' do
38+
get '/optional_custom', { :custom => 'im custom' }
39+
last_response.status.should == 200
40+
last_response.body.should == 'optional with custom works!'
41+
42+
get '/optional_custom', { :custom => 'im wrong' }
43+
last_response.status.should == 400
44+
last_response.body.should == 'custom: is not custom!'
1545
end
16-
end
17-
end
18-
19-
def app
20-
ValidationsSpec::API
21-
end
2246

23-
it 'validates optional parameter if present' do
24-
get('/', :name => "Bob", :company => "TestCorp", :a_number => "string")
25-
last_response.status.should == 400
26-
last_response.body.should == "invalid parameter: a_number"
47+
it "skip validation when parameter isn't present" do
48+
get '/optional_custom'
49+
last_response.status.should == 200
50+
last_response.body.should == 'optional with custom works!'
51+
end
52+
53+
it 'validates with custom validator when param present and incorrect type' do
54+
subject.params { optional :custom, :type => String, :customvalidator => true }
2755

28-
get('/', :name => "Bob", :company => "TestCorp", :a_number => 45)
29-
last_response.status.should == 200
30-
last_response.body.should == "Hello"
56+
get '/optional_custom', { :custom => 123 }
57+
last_response.status.should == 400
58+
last_response.body.should == 'custom: is not custom!'
59+
end
60+
end
61+
62+
context 'when using requires with a custom validator' do
63+
before do
64+
subject.params { requires :custom, :customvalidator => true }
65+
subject.get '/required_custom' do 'required with custom works!'; end
66+
end
67+
68+
it 'validates when param is present' do
69+
get '/required_custom', { :custom => 'im wrong, validate me' }
70+
last_response.status.should == 400
71+
last_response.body.should == 'custom: is not custom!'
72+
73+
get '/required_custom', { :custom => 'im custom' }
74+
last_response.status.should == 200
75+
last_response.body.should == 'required with custom works!'
76+
end
77+
78+
it 'validates when param is not present' do
79+
get '/required_custom'
80+
last_response.status.should == 400
81+
last_response.body.should == 'custom: is not custom!'
82+
end
83+
end
3184
end
32-
3385
end

0 commit comments

Comments
 (0)