Skip to content

Commit 5b51c90

Browse files
bwalexdblock
authored andcommitted
Address ruby-grape#543 - raise proper validation errors on array/hash types
* Also adds a :type option to group/requires/optional with block, which can either be Array or Hash. It defaults to Array. * Fixes (2) and (3) as mentioned in ruby-grape#543 * There is a quirk around query parameters: an empty array should pass validation if the array itself is required, but with how query parameters work, it doesn't. It does work fine with body parameters using JSON or similar, which do have a concept of an empty array.
1 parent 5a904ba commit 5b51c90

12 files changed

+330
-57
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Next Release
1313
* [#531](https://github.com/intridea/grape/pull/531): Helpers are now available to auth middleware, executing in the context of the endpoint - [@joelvh](https://github.com/joelvh).
1414
* [#540](https://github.com/intridea/grape/pull/540): Ruby 2.1.0 is now supported - [@salimane](https://github.com/salimane).
1515
* [#544](https://github.com/intridea/grape/pull/544): `rescue_from` now handles subclasses of exceptions by default - [@xevix](https://github.com/xevix).
16+
* [#545](https://github.com/intridea/grape/pull/545): Add `type` (Array or Hash) support to `requires`, `optional` and `group` with block and fix several validation issues around these - [@bwalex](https://github.com/bwalex).
1617
* Your contribution here.
1718

1819
#### Fixes

lib/grape.rb

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'rack/auth/basic'
77
require 'rack/auth/digest/md5'
88
require 'hashie'
9+
require 'set'
910
require 'active_support/core_ext/hash/indifferent_access'
1011
require 'active_support/ordered_hash'
1112
require 'active_support/core_ext/object/conversions'

lib/grape/validations.rb

+20-12
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def initialize(opts, &block)
9191
@parent = opts[:parent]
9292
@api = opts[:api]
9393
@optional = opts[:optional] || false
94+
@type = opts[:type]
9495
@declared_params = []
9596

9697
instance_eval(&block)
@@ -99,29 +100,33 @@ def initialize(opts, &block)
99100
end
100101

101102
def should_validate?(parameters)
102-
return false if @optional && params(parameters).all?(&:blank?)
103+
return false if @optional && params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
103104
return true if parent.nil?
104105
parent.should_validate?(parameters)
105106
end
106107

107108
def requires(*attrs, &block)
108-
return new_scope(attrs, &block) if block_given?
109+
orig_attrs = attrs.clone
109110

110111
validations = { presence: true }
111112
validations.merge!(attrs.pop) if attrs.last.is_a?(Hash)
112-
113-
push_declared_params(attrs)
113+
validations[:type] ||= Array if block_given?
114114
validates(attrs, validations)
115+
116+
block_given? ? new_scope(orig_attrs, &block) :
117+
push_declared_params(attrs)
115118
end
116119

117120
def optional(*attrs, &block)
118-
return new_scope(attrs, true, &block) if block_given?
121+
orig_attrs = attrs
119122

120123
validations = {}
121124
validations.merge!(attrs.pop) if attrs.last.is_a?(Hash)
122-
123-
push_declared_params(attrs)
125+
validations[:type] ||= Array if block_given?
124126
validates(attrs, validations)
127+
128+
block_given? ? new_scope(orig_attrs, true, &block) :
129+
push_declared_params(attrs)
125130
end
126131

127132
def group(element, &block)
@@ -132,9 +137,11 @@ def params(params)
132137
params = @parent.params(params) if @parent
133138
if @element
134139
if params.is_a?(Array)
135-
params = params.map { |el| el[@element] || {} }
136-
else
140+
params = params.map { |el| el[@element] || {} }.flatten
141+
elsif params.is_a?(Hash)
137142
params = params[@element] || {}
143+
else
144+
params = {}
138145
end
139146
end
140147
params
@@ -154,8 +161,9 @@ def push_declared_params(attrs)
154161
private
155162

156163
def new_scope(attrs, optional = false, &block)
157-
raise ArgumentError unless attrs.size == 1
158-
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, &block)
164+
opts = attrs[1] || { type: Array }
165+
raise ArgumentError unless opts.keys.to_set.subset? [:type].to_set
166+
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
159167
end
160168

161169
# Pushes declared params to parent or settings
@@ -237,7 +245,7 @@ def reset_validations!
237245
end
238246

239247
def params(&block)
240-
ParamsScope.new(api: self, &block)
248+
ParamsScope.new(api: self, type: Hash, &block)
241249
end
242250

243251
def document_attribute(names, opts)

lib/grape/validations/coerce.rb

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class API
66
module Validations
77
class CoerceValidator < SingleOptionValidator
88
def validate_param!(attr_name, params)
9+
raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :coerce unless params.is_a? Hash
910
new_value = coerce_value(@option, params[attr_name])
1011
if valid_type?(new_value)
1112
params[attr_name] = new_value
@@ -45,6 +46,10 @@ def valid_type?(val)
4546
end
4647

4748
def coerce_value(type, val)
49+
# Don't coerce things other than nil to Arrays or Hashes
50+
return val || [] if type == Array
51+
return val || {} if type == Hash
52+
4853
converter = Virtus::Attribute.build(type)
4954
converter.coerce(val)
5055

lib/grape/validations/presence.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def validate!(params)
77
end
88

99
def validate_param!(attr_name, params)
10-
unless params.has_key?(attr_name)
10+
unless params.respond_to?(:has_key?) && params.has_key?(attr_name)
1111
raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :presence
1212
end
1313
end

spec/grape/api_spec.rb

+3
Original file line numberDiff line numberDiff line change
@@ -1913,8 +1913,10 @@ def self.call(object, env)
19131913
subject.routes.map { |route|
19141914
route.route_params
19151915
}.should eq [{
1916+
"group1" => { required: true, type: "Array" },
19161917
"group1[param1]" => { required: false, desc: "group1 param1 desc" },
19171918
"group1[param2]" => { required: true, desc: "group1 param2 desc" },
1919+
"group2" => { required: true, type: "Array" },
19181920
"group2[param1]" => { required: false, desc: "group2 param1 desc" },
19191921
"group2[param2]" => { required: true, desc: "group2 param2 desc" }
19201922
}]
@@ -1934,6 +1936,7 @@ def self.call(object, env)
19341936
{ description: "nesting",
19351937
params: {
19361938
"root_param" => { required: true, desc: "root param" },
1939+
"nested" => { required: true, type: "Array" },
19371940
"nested[nested_param]" => { required: true, desc: "nested param" }
19381941
}
19391942
}

spec/grape/endpoint_spec.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def app
177177
requires :first
178178
optional :second
179179
optional :third, default: 'third-default'
180-
group :nested do
180+
optional :nested, type: Hash do
181181
optional :fourth
182182
end
183183
end
@@ -214,6 +214,16 @@ def app
214214
end
215215

216216
it 'builds nested params when given array' do
217+
subject.get '/dummy' do
218+
end
219+
subject.params do
220+
requires :first
221+
optional :second
222+
optional :third, default: 'third-default'
223+
optional :nested, type: Array do
224+
optional :fourth
225+
end
226+
end
217227
subject.get '/declared' do
218228
declared(params)[:nested].size.should == 2
219229
""

spec/grape/validations/coerce_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class User
191191

192192
it 'Nests integers' do
193193
subject.params do
194-
group :integers do
194+
requires :integers, type: Hash do
195195
requires :int, coerce: Integer
196196
end
197197
end

spec/grape/validations/default_spec.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ class API < Grape::API
4242
end
4343

4444
params do
45-
group :foo do
45+
# NOTE: The :foo parameter could be made required with json body
46+
# params, and then an empty hash would be valid. With query parameters
47+
# it must be optional if it isn't provided at all, as otherwise
48+
# the validaton for the Hash itself fails because there is no such
49+
# thing as an empty hash.
50+
optional :foo, type: Hash do
4651
optional :bar, default: 'foo-bar'
4752
end
4853
end

spec/grape/validations/presence_spec.rb

+13-11
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,22 @@ class API < Grape::API
2828
end
2929

3030
params do
31-
group :user do
32-
requires :first_name, :last_name
31+
requires :user, type: Hash do
32+
requires :first_name
33+
requires :last_name
3334
end
3435
end
3536
get '/nested' do
3637
"Nested"
3738
end
3839

3940
params do
40-
group :admin do
41+
requires :admin, type: Hash do
4142
requires :admin_name
42-
group :super do
43-
group :user do
44-
requires :first_name, :last_name
43+
requires :super, type: Hash do
44+
requires :user, type: Hash do
45+
requires :first_name
46+
requires :last_name
4547
end
4648
end
4749
end
@@ -96,7 +98,7 @@ def app
9698
it 'validates nested parameters' do
9799
get '/nested'
98100
last_response.status.should == 400
99-
last_response.body.should == '{"error":"user[first_name] is missing"}'
101+
last_response.body.should == '{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}'
100102

101103
get '/nested', user: { first_name: "Billy" }
102104
last_response.status.should == 400
@@ -110,19 +112,19 @@ def app
110112
it 'validates triple nested parameters' do
111113
get '/nested_triple'
112114
last_response.status.should == 400
113-
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
115+
last_response.body.should include '{"error":"admin is missing'
114116

115117
get '/nested_triple', user: { first_name: "Billy" }
116118
last_response.status.should == 400
117-
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
119+
last_response.body.should include '{"error":"admin is missing'
118120

119121
get '/nested_triple', admin: { super: { first_name: "Billy" } }
120122
last_response.status.should == 400
121-
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
123+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}'
122124

123125
get '/nested_triple', super: { user: { first_name: "Billy", last_name: "Bob" } }
124126
last_response.status.should == 400
125-
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
127+
last_response.body.should include '{"error":"admin is missing'
126128

127129
get '/nested_triple', admin: { super: { user: { first_name: "Billy" } } }
128130
last_response.status.should == 400

0 commit comments

Comments
 (0)