Skip to content

Commit 1bcbfb0

Browse files
committed
Merge pull request ruby-grape#212 from adamgotterer/master
Updates to validation and coercion: Fix ruby-grape#211 and force order of operations for presence and coercion.
2 parents 261b40d + f820ad0 commit 1bcbfb0

File tree

5 files changed

+273
-127
lines changed

5 files changed

+273
-127
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): Updates to validation and coercion: Fix #211 and force order of operations for presence and coercion - [@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: 29 additions & 10 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, required)
1212
@attrs = Array(attrs)
13+
@required = required
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 @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, required)
4144
@option = options
4245
super
4346
end
@@ -106,15 +109,31 @@ def validates(attrs, validations)
106109

107110
@api.document_attribute(attrs, doc_attrs)
108111

112+
# Validate for presence before any other validators
113+
if validations.has_key?(:presence) && validations[:presence]
114+
validate('presence', validations[:presence], attrs, doc_attrs)
115+
end
116+
117+
# Before we run the rest of the validators, lets handle
118+
# whatever coercion so that we are working with correctly
119+
# type casted values
120+
if validations.has_key? :coerce
121+
validate('coerce', validations[:coerce], attrs, doc_attrs)
122+
validations.delete(:coerce)
123+
end
124+
109125
validations.each do |type, options|
110-
validator_class = Validations::validators[type.to_s]
111-
if validator_class
112-
@api.settings[:validations] << validator_class.new(attrs, options)
113-
else
114-
raise "unknown validator: #{type}"
115-
end
126+
validate(type, options, attrs, doc_attrs)
127+
end
128+
end
129+
130+
def validate(type, options, attrs, doc_attrs)
131+
validator_class = Validations::validators[type.to_s]
132+
if validator_class
133+
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required])
134+
else
135+
raise "unknown validator: #{type}"
116136
end
117-
118137
end
119138

120139
end

spec/grape/validations/coerce_spec.rb

Lines changed: 94 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,116 @@
11
require 'spec_helper'
22

33
describe Grape::Validations::CoerceValidator do
4-
module ValidationsSpec
5-
module CoerceValidatorSpec
6-
class User
7-
include Virtus
8-
attribute :id, Integer
9-
attribute :name, String
10-
end
4+
subject { Class.new(Grape::API) }
5+
def app; subject end
116

12-
class API < Grape::API
13-
default_format :json
7+
describe 'coerce' do
8+
it 'error on malformed input' do
9+
subject.params { requires :int, :type => Integer }
10+
subject.get '/single' do 'int works'; end
1411

15-
params do
16-
requires :int, :coerce => Integer
17-
end
18-
get '/single' do
19-
end
12+
get '/single', :int => '43a'
13+
last_response.status.should == 400
14+
last_response.body.should == 'invalid parameter: int'
2015

21-
params do
22-
requires :ids, :type => Array[Integer]
23-
end
24-
get '/arr' do
25-
end
16+
get '/single', :int => '43'
17+
last_response.status.should == 200
18+
last_response.body.should == 'int works'
19+
end
2620

27-
params do
28-
requires :user, :type => ValidationsSpec::CoerceValidatorSpec::User
29-
end
30-
get '/user' do
31-
end
21+
it 'error on malformed input (Array)' do
22+
subject.params { requires :ids, :type => Array[Integer] }
23+
subject.get '/array' do 'array int works'; end
3224

33-
params do
34-
requires :int, :coerce => Integer
35-
optional :int2, :coerce => Integer
36-
optional :arr, :coerce => Array[Integer]
37-
optional :bool, :coerce => Array[Boolean]
38-
end
39-
get '/coerce' do
40-
{
41-
:int => params[:int].class,
42-
:arr => params[:arr] ? params[:arr][0].class : nil,
43-
:bool => params[:bool] ? (params[:bool][0] == true) && (params[:bool][1] == false) : nil
44-
}
45-
end
46-
params do
47-
requires :uploaded_file, :type => Rack::Multipart::UploadedFile
48-
end
49-
post '/file' do
50-
{
51-
:dpx_file => params[:uploaded_file]
52-
}
25+
get 'array', { :ids => ['1', '2', 'az'] }
26+
last_response.status.should == 400
27+
last_response.body.should == 'invalid parameter: ids'
28+
29+
get 'array', { :ids => ['1', '2', '890'] }
30+
last_response.status.should == 200
31+
last_response.body.should == 'array int works'
32+
end
33+
34+
context 'complex objects' do
35+
module CoerceValidatorSpec
36+
class User
37+
include Virtus
38+
attribute :id, Integer
39+
attribute :name, String
5340
end
5441
end
55-
end
56-
end
5742

58-
def app
59-
ValidationsSpec::CoerceValidatorSpec::API
60-
end
43+
it 'error on malformed input for complex objects' do
44+
subject.params { requires :user, :type => CoerceValidatorSpec::User }
45+
subject.get '/user' do 'complex works'; end
6146

62-
it "should return an error on malformed input" do
63-
get '/single', :int => "43a"
64-
last_response.status.should == 400
47+
get '/user', :user => "32"
48+
last_response.status.should == 400
49+
last_response.body.should == 'invalid parameter: user'
6550

66-
get '/single', :int => "43"
67-
last_response.status.should == 200
68-
end
51+
get '/user', :user => { :id => 32, :name => 'Bob' }
52+
last_response.status.should == 200
53+
last_response.body.should == 'complex works'
54+
end
55+
end
6956

70-
it "should return an error on malformed input (array)" do
71-
get '/arr', :ids => ["1", "2", "az"]
72-
last_response.status.should == 400
57+
context 'coerces' do
58+
it 'Integer' do
59+
subject.params { requires :int, :coerce => Integer }
60+
subject.get '/int' do params[:int].class; end
7361

74-
get '/arr', :ids => ["1", "2", "890"]
75-
last_response.status.should == 200
76-
end
62+
get '/int', { :int => "45" }
63+
last_response.status.should == 200
64+
last_response.body.should == 'Fixnum'
65+
end
7766

78-
it "should return an error on malformed input (complex object)" do
79-
# this request does raise an error inside Virtus
80-
get '/user', :user => "32"
81-
last_response.status.should == 400
67+
it 'Array of Integers' do
68+
subject.params { requires :arry, :coerce => Array[Integer] }
69+
subject.get '/array' do params[:arry][0].class; end
8270

83-
get '/user', :user => { :id => 32, :name => "Bob"}
84-
last_response.status.should == 200
85-
end
71+
get '/array', { :arry => [ '1', '2', '3' ] }
72+
last_response.status.should == 200
73+
last_response.body.should == 'Fixnum'
74+
end
8675

87-
it 'should coerce inputs' do
88-
get('/coerce', :int => "43", :int2 => "42")
89-
last_response.status.should == 200
90-
ret = MultiJson.load(last_response.body)
91-
ret["int"].should == "Fixnum"
92-
93-
get('/coerce', :int => "40", :int2 => "42", :arr => ["1","20","3"], :bool => [1, 0])
94-
# last_response.body.should == ""
95-
last_response.status.should == 200
96-
ret = MultiJson.load(last_response.body)
97-
ret["int"].should == "Fixnum"
98-
ret["arr"].should == "Fixnum"
99-
ret["bool"].should == true
100-
end
76+
it 'Array of Bools' do
77+
subject.params { requires :arry, :coerce => Array[Virtus::Attribute::Boolean] }
78+
subject.get '/array' do params[:arry][0].class; end
10179

102-
it 'should not return an error when an optional parameter is nil' do
103-
get('/coerce', :int => "40")
104-
last_response.status.should == 200
105-
end
80+
get 'array', { :arry => [1, 0] }
81+
last_response.status.should == 200
82+
last_response.body.should == 'TrueClass'
83+
end
84+
85+
it 'Bool' do
86+
subject.params { requires :bool, :coerce => Virtus::Attribute::Boolean }
87+
subject.get '/bool' do params[:bool].class; end
88+
89+
get '/bool', { :bool => 1 }
90+
last_response.status.should == 200
91+
last_response.body.should == 'TrueClass'
92+
93+
get '/bool', { :bool => 0 }
94+
last_response.status.should == 200
95+
last_response.body.should == 'FalseClass'
96+
97+
get '/bool', { :bool => 'false' }
98+
last_response.status.should == 200
99+
last_response.body.should == 'FalseClass'
100+
101+
get '/bool', { :bool => 'true' }
102+
last_response.status.should == 200
103+
last_response.body.should == 'TrueClass'
104+
end
105+
106+
it 'file' do
107+
subject.params { requires :file, :coerce => Rack::Multipart::UploadedFile }
108+
subject.post '/upload' do params[:file].filename; end
106109

107-
it 'should coerce a file' do
108-
post('/file', :uploaded_file => Rack::Test::UploadedFile.new(__FILE__))
109-
last_response.status.should == 201
110+
post '/upload', { :file => Rack::Test::UploadedFile.new(__FILE__) }
111+
last_response.status.should == 201
112+
last_response.body.should == File.basename(__FILE__).to_s
113+
end
114+
end
110115
end
111116
end

0 commit comments

Comments
 (0)