Skip to content

Commit b3f3eaa

Browse files
committed
Fix error messaging related to array indicies
1 parent 2d8856a commit b3f3eaa

File tree

10 files changed

+83
-27
lines changed

10 files changed

+83
-27
lines changed

lib/grape.rb

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module Exceptions
5858
extend ActiveSupport::Autoload
5959
autoload :Base
6060
autoload :Validation
61+
autoload :ValidationArrayErrors
6162
autoload :ValidationErrors
6263
autoload :MissingVendorOption
6364
autoload :MissingMimeType

lib/grape/dsl/parameters.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ def params(params)
191191
params = @parent.params(params) if @parent
192192
if @element
193193
params = if params.is_a?(Array)
194-
params.flat_map { |el| el[@element] || {} }
194+
# we can get nested arrays as result
195+
# it used for calculating parent array indicies for error messages
196+
params.map { |el| el[@element] || {} }
195197
elsif params.is_a?(Hash)
196198
params[@element] || {}
197199
else

lib/grape/endpoint.rb

+2
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ def run_validators(validators, request)
335335
validator.validate(request)
336336
rescue Grape::Exceptions::Validation => e
337337
validation_errors << e
338+
rescue Grape::Exceptions::ValidationArrayErrors => e
339+
validation_errors += e.errors
338340
end
339341
end
340342

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Grape
2+
module Exceptions
3+
class ValidationArrayErrors < Base
4+
attr_reader :errors
5+
6+
def initialize(errors)
7+
@errors = errors
8+
end
9+
end
10+
end
11+
end

lib/grape/validations/attributes_iterator.rb

+33-7
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,42 @@ class AttributesIterator
88
def initialize(validator, scope, params)
99
@scope = scope
1010
@attrs = validator.attrs
11-
@params = Array.wrap(scope.params(params))
11+
@start_params = params
12+
@original_params = scope.params(params)
13+
@params = Array.wrap(@original_params)
1214
end
1315

14-
def each
15-
@params.each do |resource_params|
16-
@attrs.each_with_index do |attr_name, index|
17-
if resource_params.is_a?(Hash) && resource_params[attr_name].is_a?(Array)
18-
scope.index = index
16+
def each(&block)
17+
do_each(@params, &block) # because we need recursion for nested arrays
18+
end
19+
20+
private
21+
22+
def do_each(params_to_process, parent_indicies = [], &block)
23+
params_to_process.each_with_index do |resource_params, index|
24+
# when we get arrays of arrays it means that target element located inside array
25+
# we need this because we want to know parent arrays indicies
26+
if resource_params.is_a?(Array)
27+
do_each(resource_params, [index] + parent_indicies, &block)
28+
next
29+
end
30+
31+
if @scope.type == Array
32+
next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array
33+
inside_array = true
34+
end
35+
if inside_array
36+
# fill current and parent scopes with correct array indicies
37+
parent_scope = @scope.parent
38+
parent_indicies.each do |parent_index|
39+
parent_scope.index = parent_index
40+
parent_scope = parent_scope.parent
1941
end
20-
yield resource_params, attr_name
42+
@scope.index = index
43+
end
44+
45+
@attrs.each do |attr_name|
46+
yield resource_params, attr_name, inside_array
2147
end
2248
end
2349
end

lib/grape/validations/params_scope.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Grape
22
module Validations
33
class ParamsScope
44
attr_accessor :element, :parent, :index
5+
attr_reader :type
56

67
include Grape::DSL::Parameters
78

@@ -57,7 +58,7 @@ def full_name(name)
5758
case
5859
when nested?
5960
# Find our containing element's name, and append ours.
60-
"#{@parent.full_name(@element)}#{parent_index}[#{name}]"
61+
"#{@parent.full_name(@element)}#{array_index}[#{name}]"
6162
when lateral?
6263
# Find the name of the element as if it was at the
6364
# same nesting level as our parent.
@@ -68,8 +69,8 @@ def full_name(name)
6869
end
6970
end
7071

71-
def parent_index
72-
"[#{@parent.index}]" if @parent.present? && @parent.index.present?
72+
def array_index
73+
"[#{@index}]" if @index.present?
7374
end
7475

7576
# @return [Boolean] whether or not this scope is the root-level scope

lib/grape/validations/validators/base.rb

+13-3
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,22 @@ def validate(request)
3434
# @raise [Grape::Exceptions::Validation] if validation failed
3535
# @return [void]
3636
def validate!(params)
37-
attributes = AttributesIterator.new(self, @scope, params)
38-
attributes.each do |resource_params, attr_name|
39-
if @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name))
37+
attributes = AttributesIterator.new(self, @scope, params)
38+
array_errors = []
39+
attributes.each do |resource_params, attr_name, inside_array|
40+
next unless @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name))
41+
42+
begin
4043
validate_param!(attr_name, resource_params)
44+
rescue Grape::Exceptions::Validation => error
45+
raise error unless inside_array
46+
# we collect errors inside array because
47+
# there may be more than one error per field
48+
array_errors << error
4149
end
4250
end
51+
52+
raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any?
4353
end
4454

4555
def self.convert_to_short_name(klass)

spec/grape/validations/validators/coerce_spec.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ class User
382382
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
383383
end
384384

385-
it 'uses parse where available' do
385+
it 'uses parse where available', focus: true do
386386
subject.params do
387387
requires :ints, type: Array, coerce_with: JSON do
388388
requires :i, type: Integer
@@ -398,6 +398,7 @@ class User
398398
expect(last_response.status).to eq(400)
399399
expect(last_response.body).to eq('ints is invalid')
400400

401+
# TODO: there is a bug in coercer. '{"i":1,"j":"2"}' transformed into [["i", 1], ["j", "2"]] WTF?
401402
get '/ints', ints: '{"i":1,"j":"2"}'
402403
expect(last_response.status).to eq(400)
403404
expect(last_response.body).to eq('ints[0][i] is missing, ints[0][i] is invalid, ints[0][j] is missing')

spec/grape/validations_spec.rb

+11-12
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def define_requires_none
245245
it 'errors when param is not an Array' do
246246
get '/required', items: 'hello'
247247
expect(last_response.status).to eq(400)
248-
expect(last_response.body).to eq('items is invalid, items[key] is missing')
248+
expect(last_response.body).to eq('items is invalid')
249249

250250
get '/required', items: { key: 'foo' }
251251
expect(last_response.status).to eq(400)
@@ -337,7 +337,7 @@ def define_requires_none
337337

338338
get '/required', items: [{ key: 'hash in array' }]
339339
expect(last_response.status).to eq(400)
340-
expect(last_response.body).to eq('items is invalid, items[0][key] does not have a valid value')
340+
expect(last_response.body).to eq('items is invalid, items[key] does not have a valid value')
341341
end
342342

343343
it 'works when all params match' do
@@ -507,12 +507,12 @@ def validate_param!(attr_name, params)
507507

508508
it 'handle errors for all array elements' do
509509
get '/within_array', children: [
510-
{ name: 'Jim', parents: [{ name: 'Joy' }] },
511-
{ name: 'Job', parents: [{ name: nil }, { name: nil }] }
510+
{ name: 'Jim', parents: [] },
511+
{ name: 'Job', parents: [] }
512512
]
513513

514514
expect(last_response.status).to eq(400)
515-
expect(last_response.body).to eq('children[1][parents][0][name] is empty, children[1][parents][1][name] is empty')
515+
expect(last_response.body).to eq('children[0][parents] is missing, children[1][parents] is missing')
516516
end
517517

518518
it 'safely handles empty arrays and blank parameters' do
@@ -527,14 +527,13 @@ def validate_param!(attr_name, params)
527527
end
528528

529529
it 'errors when param is not an Array' do
530-
# NOTE: would be nicer if these just returned 'children is invalid'
531530
get '/within_array', children: 'hello'
532531
expect(last_response.status).to eq(400)
533-
expect(last_response.body).to eq('children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing')
532+
expect(last_response.body).to eq('children is invalid')
534533

535534
get '/within_array', children: { name: 'foo' }
536535
expect(last_response.status).to eq(400)
537-
expect(last_response.body).to eq('children is invalid, children[parents] is missing')
536+
expect(last_response.body).to eq('children is invalid')
538537

539538
get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }]
540539
expect(last_response.status).to eq(400)
@@ -585,7 +584,7 @@ def validate_param!(attr_name, params)
585584
it 'requires defaults to Array type' do
586585
get '/req', planets: 'Jupiter, Saturn'
587586
expect(last_response.status).to eq(400)
588-
expect(last_response.body).to eq('planets is invalid, planets[name] is missing')
587+
expect(last_response.body).to eq('planets is invalid')
589588

590589
get '/req', planets: { name: 'Jupiter' }
591590
expect(last_response.status).to eq(400)
@@ -601,7 +600,7 @@ def validate_param!(attr_name, params)
601600
it 'optional defaults to Array type' do
602601
get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede'
603602
expect(last_response.status).to eq(400)
604-
expect(last_response.body).to eq('moons is invalid, moons[name] is missing')
603+
expect(last_response.body).to eq('moons is invalid')
605604

606605
get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' }
607606
expect(last_response.status).to eq(400)
@@ -620,7 +619,7 @@ def validate_param!(attr_name, params)
620619
it 'group defaults to Array type' do
621620
get '/grp', stars: 'Sun'
622621
expect(last_response.status).to eq(400)
623-
expect(last_response.body).to eq('stars is invalid, stars[name] is missing')
622+
expect(last_response.body).to eq('stars is invalid')
624623

625624
get '/grp', stars: { name: 'Sun' }
626625
expect(last_response.status).to eq(400)
@@ -709,7 +708,7 @@ def validate_param!(attr_name, params)
709708
it "errors when param is present but isn't an Array" do
710709
get '/optional_group', items: 'hello'
711710
expect(last_response.status).to eq(400)
712-
expect(last_response.body).to eq('items is invalid, items[key] is missing')
711+
expect(last_response.body).to eq('items is invalid')
713712

714713
get '/optional_group', items: { key: 'foo' }
715714
expect(last_response.status).to eq(400)

spec/spec_helper.rb

+3
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@
2525
config.include Rack::Test::Methods
2626
config.raise_errors_for_deprecations!
2727

28+
config.filter_run :focus
29+
config.run_all_when_everything_filtered = true
30+
2831
config.before(:each) { Grape::Util::InheritableSetting.reset_global! }
2932
end

0 commit comments

Comments
 (0)