Skip to content

Commit 5808de3

Browse files
timothysudblock
authored andcommitted
Avoid coercion of a value if it is valid (#1686)
* don't coerce param if it is valid * still run any custom coersions * fix enumerable type check * one more nil check * use coercer class instead of respond_to * style * add tests * changelog * coerce if method exists and is not json * rename to requires_coercion? * add spec
1 parent df79bc3 commit 5808de3

File tree

4 files changed

+42
-3
lines changed

4 files changed

+42
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
#### Features
44

5-
* Your contribution here.
5+
* [#1686](https://github.com/ruby-grape/grape/pull/1686): Avoid coercion of a value if it is valid - [@timothysu](https://github.com/timothysu).
66

77
#### Fixes
88

lib/grape/validations/types/custom_type_coercer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def infer_type_check(type)
137137
# passed, or if the type also implements a parse() method.
138138
type
139139
elsif type.is_a?(Enumerable)
140-
->(value) { value.all? { |item| item.is_a? type[0] } }
140+
->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } }
141141
else
142142
# By default, do a simple type check
143143
->(value) { value.is_a? type }

lib/grape/validations/validators/coerce.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def validate(request)
1616

1717
def validate_param!(attr_name, params)
1818
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless params.is_a? Hash
19+
return unless requires_coercion?(params[attr_name])
1920
new_value = coerce_value(params[attr_name])
2021
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless valid_type?(new_value)
2122
params[attr_name] = new_value
@@ -63,6 +64,11 @@ def coerce_value(val)
6364
def type
6465
@option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type]
6566
end
67+
68+
def requires_coercion?(value)
69+
# JSON types do not require coercion if value is valid
70+
!valid_type?(value) || converter.coercer.respond_to?(:method) && !converter.is_a?(Grape::Validations::Types::Json)
71+
end
6672
end
6773
end
6874
end

spec/grape/validations/validators/coerce_spec.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,23 @@ class User
419419
expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0])
420420
end
421421

422+
it 'parses parameters even if type is valid' do
423+
subject.params do
424+
requires :values, type: Array, coerce_with: ->(array) { array.map { |val| val.to_i + 1 } }
425+
end
426+
subject.get '/ints' do
427+
params[:values]
428+
end
429+
430+
get '/ints', values: [1, 2, 3, 4]
431+
expect(last_response.status).to eq(200)
432+
expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5])
433+
434+
get '/ints', values: %w(a b c d)
435+
expect(last_response.status).to eq(200)
436+
expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1])
437+
end
438+
422439
it 'uses parse where available' do
423440
subject.params do
424441
requires :ints, type: Array, coerce_with: JSON do
@@ -477,7 +494,7 @@ class User
477494
end
478495

479496
context 'first-class JSON' do
480-
it 'parses objects and arrays' do
497+
it 'parses objects, hashes, and arrays' do
481498
subject.params do
482499
requires :splines, type: JSON do
483500
requires :x, type: Integer, values: [1, 2, 3]
@@ -499,17 +516,33 @@ class User
499516
expect(last_response.status).to eq(200)
500517
expect(last_response.body).to eq('woof')
501518

519+
get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } }
520+
expect(last_response.status).to eq(200)
521+
expect(last_response.body).to eq('woof')
522+
502523
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
503524
expect(last_response.status).to eq(200)
504525
expect(last_response.body).to eq('arrays work')
505526

527+
get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
528+
expect(last_response.status).to eq(200)
529+
expect(last_response.body).to eq('arrays work')
530+
506531
get '/', splines: '{"x":4,"ints":[2]}'
507532
expect(last_response.status).to eq(400)
508533
expect(last_response.body).to eq('splines[x] does not have a valid value')
509534

535+
get '/', splines: { x: 4, ints: [2] }
536+
expect(last_response.status).to eq(400)
537+
expect(last_response.body).to eq('splines[x] does not have a valid value')
538+
510539
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
511540
expect(last_response.status).to eq(400)
512541
expect(last_response.body).to eq('splines[x] does not have a valid value')
542+
543+
get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }]
544+
expect(last_response.status).to eq(400)
545+
expect(last_response.body).to eq('splines[x] does not have a valid value')
513546
end
514547

515548
it 'works when declared optional' do

0 commit comments

Comments
 (0)