diff --git a/CHANGELOG.md b/CHANGELOG.md index ad32abbc99..6ca67c53a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * Your contribution here. +* [#2157](https://github.com/ruby-grape/grape/pull/2157): Custom types can set a message to be used in the response when invalid - [@dnesteryuk](https://github.com/dnesteryuk). * [#2145](https://github.com/ruby-grape/grape/pull/2145): Ruby 3.0 compatibility - [@ericproulx](https://github.com/ericproulx). * [#2143](https://github.com/ruby-grape/grape/pull/2143): Enable GitHub Actions with updated RuboCop and Danger - [@anakinj](https://github.com/anakinj). diff --git a/Gemfile b/Gemfile index 6c4124a52d..8e32048c45 100644 --- a/Gemfile +++ b/Gemfile @@ -35,3 +35,7 @@ group :test do gem 'rspec', '~> 3.0' gem 'ruby-grape-danger', '~> 0.2.0', require: false end + +platforms :jruby do + gem 'racc' +end diff --git a/README.md b/README.md index 2f9a0bae63..2247110582 100644 --- a/README.md +++ b/README.md @@ -1183,7 +1183,8 @@ Aside from the default set of supported types listed above, any class can be used as a type as 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, or return an instance of `Grape::Types::InvalidValue` which optionally +accepts a message to be returned in the response. ```ruby class Color @@ -1193,8 +1194,9 @@ class Color end def self.parse(value) - fail 'Invalid color' unless %w(blue red green).include?(value) - new(value) + return new(value) if %w[blue red green]).include?(value) + + Grape::Types::InvalidValue.new('Unsupported color') end end diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index a682286a56..2240a7fa75 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -7,6 +7,7 @@ require_relative 'types/variant_collection_coercer' require_relative 'types/json' require_relative 'types/file' +require_relative 'types/invalid_value' module Grape module Validations @@ -21,10 +22,6 @@ module Validations # and {Grape::Dsl::Parameters#optional}. The main # entry point for this process is {Types.build_coercer}. module Types - # Instances of this class may be used as tokens to denote that - # a parameter value could not be coerced. - class InvalidValue; end - # Types representing a single value, which are coerced. PRIMITIVES = [ # Numerical diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index a11403e1c6..f1fd4a80c9 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -55,6 +55,8 @@ def call(val) return if val.nil? coerced_val = @method.call(val) + + return coerced_val if coerced_val.is_a?(InvalidValue) return InvalidValue.new unless coerced?(coerced_val) coerced_val end diff --git a/lib/grape/validations/types/invalid_value.rb b/lib/grape/validations/types/invalid_value.rb new file mode 100644 index 0000000000..5c566a642c --- /dev/null +++ b/lib/grape/validations/types/invalid_value.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Grape + module Validations + module Types + # Instances of this class may be used as tokens to denote that a parameter value could not be + # coerced. The given message will be used as a validation error. + class InvalidValue + attr_reader :message + + def initialize(message = nil) + @message = message + end + end + end + end +end + +# only exists to make it shorter for external use +module Grape + module Types + InvalidValue = Class.new(Grape::Validations::Types::InvalidValue) + end +end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index 1b15c069d5..f79b4fa6ec 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -36,7 +36,7 @@ def validate_param!(attr_name, params) new_value = coerce_value(params[attr_name]) - raise validation_exception(attr_name) unless valid_type?(new_value) + raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash # which looses wrappers for hashes and arrays after reassigning values @@ -80,8 +80,11 @@ def type @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end - def validation_exception(attr_name) - Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:coerce)) + def validation_exception(attr_name, custom_msg = nil) + Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: custom_msg || message(:coerce) + ) end end end diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index e157748e91..a9d3123a94 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -227,23 +227,51 @@ def self.parsed?(value) expect(last_response.body).to eq('NilClass') end - it 'is a custom type' do - subject.params do - requires :uri, coerce: SecureURIOnly - end - subject.get '/secure_uri' do - params[:uri].class + context 'a custom type' do + it 'coerces the given value' do + subject.params do + requires :uri, coerce: SecureURIOnly + end + subject.get '/secure_uri' do + params[:uri].class + end + + get 'secure_uri', uri: 'https://www.example.com' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('URI::HTTPS') + + get 'secure_uri', uri: 'http://www.example.com' + + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('uri is invalid') end - get 'secure_uri', uri: 'https://www.example.com' + context 'returning the InvalidValue instance when invalid' do + let(:custom_type) do + Class.new do + def self.parse(_val) + Grape::Types::InvalidValue.new('must be unique') + end + end + end - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('URI::HTTPS') + it 'uses a custom message added to the invalid value' do + type = custom_type + + subject.params do + requires :name, type: type + end + subject.get '/whatever' do + params[:name].class + end - get 'secure_uri', uri: 'http://www.example.com' + get 'whatever', name: 'Bob' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('uri is invalid') + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('name must be unique') + end + end end context 'Array' do