-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updates to validation and coercion: Fix #211 and force order of operations for presence and coercion. #212
Changes from all commits
5ec9c57
e77d878
61fdcb7
5f92a0c
f820ad0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,9 @@ module Validations | |
# All validators must inherit from this class. | ||
# | ||
class Validator | ||
def initialize(attrs, options) | ||
def initialize(attrs, options, required) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to add this instead of allowing options to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Options are the options passed to the validator. Like min/man if you were doing length validation. Don't think required would be good to add there. Originally I had this as doc_attrs and decided that it didn't seem appropriate to pass that around. Could expand required to a different set of options though? |
||
@attrs = Array(attrs) | ||
@required = required | ||
|
||
if options.is_a?(Hash) && !options.empty? | ||
raise "unknown options: #{options.keys}" | ||
|
@@ -18,7 +19,9 @@ def initialize(attrs, options) | |
|
||
def validate!(params) | ||
@attrs.each do |attr_name| | ||
validate_param!(attr_name, params) | ||
if @required || params.has_key?(attr_name) | ||
validate_param!(attr_name, params) | ||
end | ||
end | ||
end | ||
|
||
|
@@ -37,7 +40,7 @@ def self.convert_to_short_name(klass) | |
## | ||
# Base class for all validators taking only one param. | ||
class SingleOptionValidator < Validator | ||
def initialize(attrs, options) | ||
def initialize(attrs, options, required) | ||
@option = options | ||
super | ||
end | ||
|
@@ -106,15 +109,31 @@ def validates(attrs, validations) | |
|
||
@api.document_attribute(attrs, doc_attrs) | ||
|
||
# Validate for presence before any other validators | ||
if validations.has_key?(:presence) && validations[:presence] | ||
validate('presence', validations[:presence], attrs, doc_attrs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just realized that I need to remove presence from the validation array. Shouldn't affect it negatively. Would just cause it to validate twice. I'll fix that with the next batch of updates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for your hard work, Adam. |
||
end | ||
|
||
# Before we run the rest of the validators, lets handle | ||
# whatever coercion so that we are working with correctly | ||
# type casted values | ||
if validations.has_key? :coerce | ||
validate('coerce', validations[:coerce], attrs, doc_attrs) | ||
validations.delete(:coerce) | ||
end | ||
|
||
validations.each do |type, options| | ||
validator_class = Validations::validators[type.to_s] | ||
if validator_class | ||
@api.settings[:validations] << validator_class.new(attrs, options) | ||
else | ||
raise "unknown validator: #{type}" | ||
end | ||
validate(type, options, attrs, doc_attrs) | ||
end | ||
end | ||
|
||
def validate(type, options, attrs, doc_attrs) | ||
validator_class = Validations::validators[type.to_s] | ||
if validator_class | ||
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required]) | ||
else | ||
raise "unknown validator: #{type}" | ||
end | ||
|
||
end | ||
|
||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,111 +1,116 @@ | ||
require 'spec_helper' | ||
|
||
describe Grape::Validations::CoerceValidator do | ||
module ValidationsSpec | ||
module CoerceValidatorSpec | ||
class User | ||
include Virtus | ||
attribute :id, Integer | ||
attribute :name, String | ||
end | ||
subject { Class.new(Grape::API) } | ||
def app; subject end | ||
|
||
class API < Grape::API | ||
default_format :json | ||
describe 'coerce' do | ||
it 'error on malformed input' do | ||
subject.params { requires :int, :type => Integer } | ||
subject.get '/single' do 'int works'; end | ||
|
||
params do | ||
requires :int, :coerce => Integer | ||
end | ||
get '/single' do | ||
end | ||
get '/single', :int => '43a' | ||
last_response.status.should == 400 | ||
last_response.body.should == 'invalid parameter: int' | ||
|
||
params do | ||
requires :ids, :type => Array[Integer] | ||
end | ||
get '/arr' do | ||
end | ||
get '/single', :int => '43' | ||
last_response.status.should == 200 | ||
last_response.body.should == 'int works' | ||
end | ||
|
||
params do | ||
requires :user, :type => ValidationsSpec::CoerceValidatorSpec::User | ||
end | ||
get '/user' do | ||
end | ||
it 'error on malformed input (Array)' do | ||
subject.params { requires :ids, :type => Array[Integer] } | ||
subject.get '/array' do 'array int works'; end | ||
|
||
params do | ||
requires :int, :coerce => Integer | ||
optional :int2, :coerce => Integer | ||
optional :arr, :coerce => Array[Integer] | ||
optional :bool, :coerce => Array[Boolean] | ||
end | ||
get '/coerce' do | ||
{ | ||
:int => params[:int].class, | ||
:arr => params[:arr] ? params[:arr][0].class : nil, | ||
:bool => params[:bool] ? (params[:bool][0] == true) && (params[:bool][1] == false) : nil | ||
} | ||
end | ||
params do | ||
requires :uploaded_file, :type => Rack::Multipart::UploadedFile | ||
end | ||
post '/file' do | ||
{ | ||
:dpx_file => params[:uploaded_file] | ||
} | ||
get 'array', { :ids => ['1', '2', 'az'] } | ||
last_response.status.should == 400 | ||
last_response.body.should == 'invalid parameter: ids' | ||
|
||
get 'array', { :ids => ['1', '2', '890'] } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'array int works' | ||
end | ||
|
||
context 'complex objects' do | ||
module CoerceValidatorSpec | ||
class User | ||
include Virtus | ||
attribute :id, Integer | ||
attribute :name, String | ||
end | ||
end | ||
end | ||
end | ||
|
||
def app | ||
ValidationsSpec::CoerceValidatorSpec::API | ||
end | ||
it 'error on malformed input for complex objects' do | ||
subject.params { requires :user, :type => CoerceValidatorSpec::User } | ||
subject.get '/user' do 'complex works'; end | ||
|
||
it "should return an error on malformed input" do | ||
get '/single', :int => "43a" | ||
last_response.status.should == 400 | ||
get '/user', :user => "32" | ||
last_response.status.should == 400 | ||
last_response.body.should == 'invalid parameter: user' | ||
|
||
get '/single', :int => "43" | ||
last_response.status.should == 200 | ||
end | ||
get '/user', :user => { :id => 32, :name => 'Bob' } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'complex works' | ||
end | ||
end | ||
|
||
it "should return an error on malformed input (array)" do | ||
get '/arr', :ids => ["1", "2", "az"] | ||
last_response.status.should == 400 | ||
context 'coerces' do | ||
it 'Integer' do | ||
subject.params { requires :int, :coerce => Integer } | ||
subject.get '/int' do params[:int].class; end | ||
|
||
get '/arr', :ids => ["1", "2", "890"] | ||
last_response.status.should == 200 | ||
end | ||
get '/int', { :int => "45" } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'Fixnum' | ||
end | ||
|
||
it "should return an error on malformed input (complex object)" do | ||
# this request does raise an error inside Virtus | ||
get '/user', :user => "32" | ||
last_response.status.should == 400 | ||
it 'Array of Integers' do | ||
subject.params { requires :arry, :coerce => Array[Integer] } | ||
subject.get '/array' do params[:arry][0].class; end | ||
|
||
get '/user', :user => { :id => 32, :name => "Bob"} | ||
last_response.status.should == 200 | ||
end | ||
get '/array', { :arry => [ '1', '2', '3' ] } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'Fixnum' | ||
end | ||
|
||
it 'should coerce inputs' do | ||
get('/coerce', :int => "43", :int2 => "42") | ||
last_response.status.should == 200 | ||
ret = MultiJson.load(last_response.body) | ||
ret["int"].should == "Fixnum" | ||
|
||
get('/coerce', :int => "40", :int2 => "42", :arr => ["1","20","3"], :bool => [1, 0]) | ||
# last_response.body.should == "" | ||
last_response.status.should == 200 | ||
ret = MultiJson.load(last_response.body) | ||
ret["int"].should == "Fixnum" | ||
ret["arr"].should == "Fixnum" | ||
ret["bool"].should == true | ||
end | ||
it 'Array of Bools' do | ||
subject.params { requires :arry, :coerce => Array[Virtus::Attribute::Boolean] } | ||
subject.get '/array' do params[:arry][0].class; end | ||
|
||
it 'should not return an error when an optional parameter is nil' do | ||
get('/coerce', :int => "40") | ||
last_response.status.should == 200 | ||
end | ||
get 'array', { :arry => [1, 0] } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'TrueClass' | ||
end | ||
|
||
it 'Bool' do | ||
subject.params { requires :bool, :coerce => Virtus::Attribute::Boolean } | ||
subject.get '/bool' do params[:bool].class; end | ||
|
||
get '/bool', { :bool => 1 } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'TrueClass' | ||
|
||
get '/bool', { :bool => 0 } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'FalseClass' | ||
|
||
get '/bool', { :bool => 'false' } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'FalseClass' | ||
|
||
get '/bool', { :bool => 'true' } | ||
last_response.status.should == 200 | ||
last_response.body.should == 'TrueClass' | ||
end | ||
|
||
it 'file' do | ||
subject.params { requires :file, :coerce => Rack::Multipart::UploadedFile } | ||
subject.post '/upload' do params[:file].filename; end | ||
|
||
it 'should coerce a file' do | ||
post('/file', :uploaded_file => Rack::Test::UploadedFile.new(__FILE__)) | ||
last_response.status.should == 201 | ||
post '/upload', { :file => Rack::Test::UploadedFile.new(__FILE__) } | ||
last_response.status.should == 201 | ||
last_response.body.should == File.basename(__FILE__).to_s | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a contrived example. It would be nice to find something more "useful".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a shot at updated the whole section and improving my example. I've learned a lot of things that you can do that we haven't documented.