diff --git a/CHANGELOG.md b/CHANGELOG.md index dd47d320ed..953c592af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#1390](https://github.com/ruby-grape/grape/pull/1390): Allow inserting middleware at arbitrary points in the middleware stack - [@Rosa](https://github.com/Rosa). * [#1366](https://github.com/ruby-grape/grape/pull/1366): Store `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou). * [#1398](https://github.com/ruby-grape/grape/pull/1398): Added `rescue_from :grape_exceptions` - allow Grape to use the built-in `Grape::Exception` handing and use `rescue :all` behavior for everything else - [@mmclead](https://github.com/mmclead). +* [#1203](https://github.com/ruby-grape/grape/pull/1203): Allow custom coercion failure messages - [@zuk](https://github.com/zuk). * Your contribution here. #### Fixes diff --git a/README.md b/README.md index fd0bd1f311..f30ba411e8 100644 --- a/README.md +++ b/README.md @@ -803,7 +803,9 @@ Aside from the default set of supported types listed above, any class can be used as a type so long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct -type, or raise an exception to indicate the value was invalid. E.g., +type. An exception raised inside the `parse` method will be reported as a validation +failure with a generic error message. To report a custom error message, return an +`InvalidValue` initialized with the custom message. E.g., ```ruby class Color @@ -813,8 +815,11 @@ class Color end def self.parse(value) - fail 'Invalid color' unless %w(blue red green).include?(value) - new(value) + if %w(blue red green).include?(value) + new(value) + else + Grape::Validations::Types::InvalidValue.new "is not a valid color" + end end end diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index 7cd4118b85..568c7f9ae2 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -24,7 +24,12 @@ module Validations module Types # Instances of this class may be used as tokens to denote that # a parameter value could not be coerced. - class InvalidValue; end + class InvalidValue + attr_reader :message + def initialize(message = nil) + @message = message + end + end # Types representing a single value, which are coerced through Virtus # or special logic in Grape. diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index f4710a6bb6..d4556a45e3 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -13,8 +13,16 @@ def initialize(*_args) def validate_param!(attr_name, params) raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless params.is_a? Hash new_value = coerce_value(params[attr_name]) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless valid_type?(new_value) - params[attr_name] = new_value + if valid_type?(new_value) + params[attr_name] = new_value + else + bad_value = new_value + if bad_value.is_a?(Types::InvalidValue) && !bad_value.message.nil? + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: bad_value.message.to_s + else + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :coerce + end + end end private diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index 693205ca90..fd3f0a463c 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -119,6 +119,54 @@ def initialize(value) expect(last_response.status).to eq(400) expect(last_response.body).to match(/foo is invalid/) end + + context 'when the parse method returns an InvalidValue' do + module ParamsScopeSpec + class AnotherCustomType + attr_reader :value + def self.parse(value) + case value + when 'invalid with message' + Grape::Validations::Types::InvalidValue.new 'is not correct' + when 'invalid without message' + Grape::Validations::Types::InvalidValue.new + else + new(value) + end + end + + def initialize(value) + @value = value + end + end + end + + context 'with a message' do + it 'fails with the InvalidValue\'s error message' do + subject.params do + requires :foo, type: ParamsScopeSpec::AnotherCustomType + end + subject.get('/types') { params[:foo].value } + + get '/types', foo: 'invalid with message' + expect(last_response.status).to eq(400) + expect(last_response.body).to match(/foo is not correct/) + end + end + + context 'without a message' do + it 'fails with the default coercion failure message' do + subject.params do + requires :foo, type: ParamsScopeSpec::AnotherCustomType + end + subject.get('/types') { params[:foo].value } + + get '/types', foo: 'invalid without message' + expect(last_response.status).to eq(400) + expect(last_response.body).to match(/foo is invalid/) + end + end + end end context 'array without coerce type explicitly given' do