Skip to content

Commit 70d07d9

Browse files
committed
Fix array indicies in error messages. (#1463)
1 parent 5804b28 commit 70d07d9

File tree

12 files changed

+108
-29
lines changed

12 files changed

+108
-29
lines changed

.rubocop_todo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Metrics/BlockNesting:
1717
# Offense count: 7
1818
# Configuration parameters: CountComments.
1919
Metrics/ClassLength:
20-
Max: 264
20+
Max: 266
2121

2222
# Offense count: 24
2323
Metrics/CyclomaticComplexity:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#### Features
55

6+
* [#1463](https://github.com/ruby-grape/grape/pull/1463): Fix array indicies in error messages - [@ffloyd](https://github.com/ffloyd).
67
* [#1393](https://github.com/ruby-grape/grape/pull/1393): Middleware can be inserted before or after default Grape middleware - [@ridiculous](https://github.com/ridiculous).
78
* [#1390](https://github.com/ruby-grape/grape/pull/1390): Allows inserting middleware at arbitrary points in the middleware stack - [@Rosa](https://github.com/Rosa).
89
* [#1366](https://github.com/ruby-grape/grape/pull/1366): Stores `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou).

lib/grape.rb

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ 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+
# used for calculating parent array indices for error messages
195+
params.map { |el| el[@element] || {} }
195196
elsif params.is_a?(Hash)
196197
params[@element] || {}
197198
else

lib/grape/endpoint.rb

Lines changed: 2 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,41 @@ class AttributesIterator
88
def initialize(validator, scope, params)
99
@scope = scope
1010
@attrs = validator.attrs
11-
@params = Array.wrap(scope.params(params))
11+
@original_params = scope.params(params)
12+
@params = Array.wrap(@original_params)
1213
end
1314

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

lib/grape/validations/params_scope.rb

Lines changed: 4 additions & 3 deletions
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/types/build_coercer.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ def self.build_coercer(type, method = nil)
2626
converter_options = {
2727
nullify_blank: true
2828
}
29-
conversion_type = type
29+
conversion_type = if method == JSON
30+
Object
31+
# because we want just parsed JSON content:
32+
# if type is Array and data is `"{}"`
33+
# result will be [] because Virtus converts hashes
34+
# to arrays
35+
else
36+
type
37+
end
3038

3139
# Use a special coercer for multiply-typed parameters.
3240
if Types.multiple?(type)

lib/grape/validations/validators/base.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,21 @@ def validate(request)
3535
# @return [void]
3636
def validate!(params)
3737
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))
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 => e
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 << e
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)

0 commit comments

Comments
 (0)