Skip to content

Commit

Permalink
Merge pull request #2128 from dwhenry/fix-nested-validations
Browse files Browse the repository at this point in the history
Fix validation error for nested array when  `requires` => `optional` => `requires`
  • Loading branch information
dblock authored Nov 5, 2020
2 parents 338663d + 2647e2c commit d9d99ea
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#### Fixes

* Your contribution here.
* [#2128](https://github.com/ruby-grape/grape/pull/2128): Fix validation error when Required Array nested inside an optional array - [@dwhenry](https://github.com/dwhenry).
* [#2127](https://github.com/ruby-grape/grape/pull/2127): Fix a performance issue with dependent params - [@dnesteryuk](https://github.com/dnesteryuk).
* [#2126](https://github.com/ruby-grape/grape/pull/2126): Fix warnings about redefined attribute accessors in `AttributeTranslator` - [@samsonjs](https://github.com/samsonjs).
* [#2121](https://github.com/ruby-grape/grape/pull/2121): Fix 2.7 deprecation warning in validator_factory - [@Legogris](https://github.com/Legogris).
Expand Down
10 changes: 7 additions & 3 deletions lib/grape/dsl/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,17 @@ def declared_param?(param)

alias group requires

def map_params(params, element)
class EmptyOptionalValue; end

def map_params(params, element, is_array = false)
if params.is_a?(Array)
params.map do |el|
map_params(el, element)
map_params(el, element, true)
end
elsif params.is_a?(Hash)
params[element] || {}
params[element] || (@optional && is_array ? EmptyOptionalValue : {})
elsif params == EmptyOptionalValue
EmptyOptionalValue
else
{}
end
Expand Down
11 changes: 10 additions & 1 deletion lib/grape/validations/single_attribute_iterator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ class SingleAttributeIterator < AttributesIterator

def yield_attributes(val, attrs)
attrs.each do |attr_name|
yield val, attr_name, empty?(val)
yield val, attr_name, empty?(val), skip?(val)
end
end


# This is a special case so that we can ignore tree's where option
# values are missing lower down. Unfortunately we can remove this
# are the parameter parsing stage as they are required to ensure
# the correct indexing is maintained
def skip?(val)
val == Grape::DSL::Parameters::EmptyOptionalValue
end

# Primitives like Integers and Booleans don't respond to +empty?+.
# It could be possible to use +blank?+ instead, but
#
Expand Down
3 changes: 2 additions & 1 deletion lib/grape/validations/validators/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def validate!(params)
# there may be more than one error per field
array_errors = []

attributes.each do |val, attr_name, empty_val|
attributes.each do |val, attr_name, empty_val, skip_value|
next if skip_value
next if !@scope.required? && empty_val
next unless @scope.meets_dependency?(val, params)
begin
Expand Down
23 changes: 17 additions & 6 deletions spec/grape/validations/single_attribute_iterator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

it 'yields params and every single attribute from the list' do
expect { |b| iterator.each(&b) }
.to yield_successive_args([params, :first, false], [params, :second, false])
.to yield_successive_args([params, :first, false, false], [params, :second, false, false])
end
end

Expand All @@ -26,8 +26,8 @@

it 'yields every single attribute from the list for each of the array elements' do
expect { |b| iterator.each(&b) }.to yield_successive_args(
[params[0], :first, false], [params[0], :second, false],
[params[1], :first, false], [params[1], :second, false]
[params[0], :first, false, false], [params[0], :second, false, false],
[params[1], :first, false, false], [params[1], :second, false, false]
)
end

Expand All @@ -36,9 +36,20 @@

it 'marks params with empty values' do
expect { |b| iterator.each(&b) }.to yield_successive_args(
[params[0], :first, true], [params[0], :second, true],
[params[1], :first, true], [params[1], :second, true],
[params[2], :first, false], [params[2], :second, false]
[params[0], :first, true, false], [params[0], :second, true, false],
[params[1], :first, true, false], [params[1], :second, true, false],
[params[2], :first, false, false], [params[2], :second, false, false]
)
end
end

context 'when missing optional value' do
let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] }

it 'marks params with skipped values' do
expect { |b| iterator.each(&b) }.to yield_successive_args(
[params[0], :first, false, true], [params[0], :second, false, true],
[params[1], :first, false, false], [params[1], :second, false, false],
)
end
end
Expand Down
184 changes: 184 additions & 0 deletions spec/grape/validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,190 @@ def validate_param!(attr_name, params)
end
expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
end

context <<~DESC do
Issue occurs whenever:
* param structure with at least three levels
* 1st level item is a required Array that has >1 entry with an optional item present and >1 entry with an optional item missing
* 2nd level is an optional Array or Hash
* 3rd level is a required item (can be any type)
* additional levels do not effect the issue from occuring
DESC

it "example based off actual real world use case" do
subject.params do
requires :orders, type: Array do
requires :id, type: Integer
optional :drugs, type: Array do
requires :batches, type: Array do
requires :batch_no, type: String
end
end
end
end

subject.get '/validate_required_arrays_under_optional_arrays' do
'validate_required_arrays_under_optional_arrays works!'
end

data = {
orders: [
{ id: 77, drugs: [{batches: [{batch_no: "A1234567"}]}]},
{ id: 70 }
]
}

get '/validate_required_arrays_under_optional_arrays', data
expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
expect(last_response.status).to eq(200)
end

it "simplest example using Arry -> Array -> Hash -> String" do
subject.params do
requires :orders, type: Array do
requires :id, type: Integer
optional :drugs, type: Array do
requires :batch_no, type: String
end
end
end

subject.get '/validate_required_arrays_under_optional_arrays' do
'validate_required_arrays_under_optional_arrays works!'
end

data = {
orders: [
{ id: 77, drugs: [{batch_no: "A1234567"}]},
{ id: 70 }
]
}

get '/validate_required_arrays_under_optional_arrays', data
expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
expect(last_response.status).to eq(200)
end

it "simplest example using Arry -> Hash -> String" do
subject.params do
requires :orders, type: Array do
requires :id, type: Integer
optional :drugs, type: Hash do
requires :batch_no, type: String
end
end
end

subject.get '/validate_required_arrays_under_optional_arrays' do
'validate_required_arrays_under_optional_arrays works!'
end

data = {
orders: [
{ id: 77, drugs: {batch_no: "A1234567"}},
{ id: 70 }
]
}

get '/validate_required_arrays_under_optional_arrays', data
expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
expect(last_response.status).to eq(200)
end

it "correctly indexes invalida data" do
subject.params do
requires :orders, type: Array do
requires :id, type: Integer
optional :drugs, type: Array do
requires :batch_no, type: String
requires :quantity, type: Integer
end
end
end

subject.get '/correctly_indexes' do
'correctly_indexes works!'
end

data = {
orders: [
{ id: 70 },
{ id: 77, drugs: [{batch_no: "A1234567", quantity: 12}, {batch_no: "B222222"}]}
]
}

get '/correctly_indexes', data
expect(last_response.body).to eq("orders[1][drugs][1][quantity] is missing")
expect(last_response.status).to eq(400)
end

context "multiple levels of optional and requires settings" do
before do
subject.params do
requires :top, type: Array do
requires :top_id, type: Integer, allow_blank: false
optional :middle_1, type: Array do
requires :middle_1_id, type: Integer, allow_blank: false
optional :middle_2, type: Array do
requires :middle_2_id, type: String, allow_blank: false
optional :bottom, type: Array do
requires :bottom_id, type: Integer, allow_blank: false
end
end
end
end
end

subject.get '/multi_level' do
'multi_level works!'
end
end

it "with valid data" do
data_without_errors = {
top: [
{ top_id: 1, middle_1: [
{middle_1_id: 11}, {middle_1_id: 12, middle_2: [
{middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: 1221}]}]}]},
{ top_id: 2, middle_1: [
{middle_1_id: 21}, {middle_1_id: 22, middle_2: [
{middle_2_id: 221}]}]},
{ top_id: 3, middle_1: [
{middle_1_id: 31}, {middle_1_id: 32}]},
{ top_id: 4 }
]
}

get '/multi_level', data_without_errors
expect(last_response.body).to eq("multi_level works!")
expect(last_response.status).to eq(200)
end

it "with invalid data" do
data = {
top: [
{ top_id: 1, middle_1: [
{middle_1_id: 11}, {middle_1_id: 12, middle_2: [
{middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: nil}]}]}]},
{ top_id: 2, middle_1: [
{middle_1_id: 21}, {middle_1_id: 22, middle_2: [{middle_2_id: nil}]}]},
{ top_id: 3, middle_1: [
{middle_1_id: nil}, {middle_1_id: 32}]},
{ top_id: nil, missing_top_id: 4 }
]
}
# debugger
get '/multi_level', data
expect(last_response.body.split(", ")).to match_array([
"top[3][top_id] is empty",
"top[2][middle_1][0][middle_1_id] is empty",
"top[1][middle_1][1][middle_2][0][middle_2_id] is empty",
"top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty"
])
expect(last_response.status).to eq(400)
end
end
end
end

context 'multiple validation errors' do
Expand Down

0 comments on commit d9d99ea

Please sign in to comment.