diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c9ff2be6..fb19c7376a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * [#1255](https://github.com/ruby-grape/grape/pull/1255): Allow param type definition in `route_param` - [@namusyaka](https://github.com/namusyaka). * [#1257](https://github.com/ruby-grape/grape/pull/1257): Allow Proc, Symbol or String in `rescue_from with: ...` - [@namusyaka](https://github.com/namusyaka). * [#1285](https://github.com/ruby-grape/grape/pull/1285): Add a warning for errors appearing in `after` callbacks - [@gregormelhorn](https://github.com/gregormelhorn). -* Your contribution here. +* [#1295](https://github.com/ruby-grape/grape/pull/1295): Add custom validation messages for parameter exceptions - [@railsmith](https://github.com/railsmith). #### Fixes diff --git a/README.md b/README.md index d62571c082..b7e7a86787 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ - [Custom Validators](#custom-validators) - [Validation Errors](#validation-errors) - [I18n](#i18n) + - [Custom Validation Messages](#custom-validation-messages) - [Headers](#headers) - [Routes](#routes) - [Helpers](#helpers) @@ -1237,6 +1238,119 @@ end Grape supports I18n for parameter-related error messages, but will fallback to English if translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys. +### Custom Validation messages + +Grape supports custom validation messages for parameter-related and coerce-related error messages. + +#### `presence`, `allow_blank`, `values`, `regexp` + +```ruby +params do + requires :name, values: { value: 1..10, message: 'not in range from 1 to 10' }, allow_blank: { value: false, message: 'cannot be blank' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }, message: 'is required' +end +``` +#### `all_or_none_of` + +```ruby +params do + optional :beer + optional :wine + optional :juice + all_or_none_of :beer, :wine, :juice, message: "all params are required or none is required" +end +``` + +#### `mutually_exclusive` + +```ruby +params do + optional :beer + optional :wine + optional :juice + mutually_exclusive :beer, :wine, :juice, message: "are mutually exclusive cannot pass both params" +end +``` +#### `exactly_one_of` + +```ruby +params do + optional :beer + optional :wine + optional :juice + exactly_one_of :beer, :wine, :juice, message: {exactly_one: "are missing, exactly one parameter is required", mutual_exclusion: "are mutually exclusive, exactly one parameter is required"} +end +``` +#### `at_least_one_of` + +```ruby +params do + optional :beer + optional :wine + optional :juice + at_least_one_of :beer, :wine, :juice, message: "are missing, please specify at least one param" +end +``` +#### `Coerce` + +```ruby +params do + requires :int, type: {value: Integer, message: "type cast is invalid" } +end +``` +#### `With Lambdas` + +```ruby +params do + requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' } +end +``` +#### `Pass symbols for i18n translations` + +You can pass a symbol if you want i18n translations for your custom validation messages. + +```ruby +params do + requires :name, message: :name_required +end +``` +```ruby +# en.yml + +en: + grape: + errors: + format: ! '%{attributes} %{message}' + messages: + name_required: 'must be present' +``` + +#### `Overriding attribute names` + +You can also override attribute names. + +```ruby +# en.yml + +en: + grape: + errors: + format: ! '%{attributes} %{message}' + messages: + name_required: 'must be present' + attributes: + name: 'Oops! Name' +``` +Will produce 'Oops! Name must be present' + +#### `With Default` + +You cannot set a custom message option for Default as it requires interpolation `%{option1}: %{value1} is incompatible with %{option2}: %{value2}`. You can change the default error message for Default by changing the `incompatible_option_values` message key inside [en.yml](lib/grape/locale/en.yml) + +```ruby +params do + requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' }, default: 5 +end +``` ## Headers Request headers are available through the `headers` helper or from `env` in their original form. diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index 5734f8e228..2a4b4be156 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -98,7 +98,7 @@ def requires(*attrs, &block) orig_attrs = attrs.clone opts = attrs.extract_options!.clone - opts[:presence] = true + opts[:presence] = { value: true, message: opts[:message] } if opts[:using] require_required_and_optional_fields(attrs.first, opts) @@ -137,25 +137,25 @@ def optional(*attrs, &block) # Disallow the given parameters to be present in the same request. # @param attrs [*Symbol] parameters to validate def mutually_exclusive(*attrs) - validates(attrs, mutual_exclusion: true) + validates(attrs, mutual_exclusion: { value: true, message: extract_message_option(attrs) }) end # Require exactly one of the given parameters to be present. # @param (see #mutually_exclusive) def exactly_one_of(*attrs) - validates(attrs, exactly_one_of: true) + validates(attrs, exactly_one_of: { value: true, message: extract_message_option(attrs) }) end # Require at least one of the given parameters to be present. # @param (see #mutually_exclusive) def at_least_one_of(*attrs) - validates(attrs, at_least_one_of: true) + validates(attrs, at_least_one_of: { value: true, message: extract_message_option(attrs) }) end # Require that either all given params are present, or none are. # @param (see #mutually_exclusive) def all_or_none_of(*attrs) - validates(attrs, all_or_none_of: true) + validates(attrs, all_or_none_of: { value: true, message: extract_message_option(attrs) }) end # Define a block of validations which should be applied if and only if diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index b5df55a231..0cb2ba6b82 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -38,15 +38,15 @@ def compose_message(key, attributes = {}) end def problem(key, attributes) - translate_message("#{key}.problem", attributes) + translate_message("#{key}.problem".to_sym, attributes) end def summary(key, attributes) - translate_message("#{key}.summary", attributes) + translate_message("#{key}.summary".to_sym, attributes) end def resolution(key, attributes) - translate_message("#{key}.resolution", attributes) + translate_message("#{key}.resolution".to_sym, attributes) end def translate_attributes(keys, options = {}) @@ -60,7 +60,14 @@ def translate_attribute(key, options = {}) end def translate_message(key, options = {}) - translate("#{BASE_MESSAGES_KEY}.#{key}", options.reverse_merge(default: '')) + case key + when Symbol + translate("#{BASE_MESSAGES_KEY}.#{key}", options.reverse_merge(default: '')) + when Proc + key.call + else + key + end end def translate(key, options = {}) diff --git a/lib/grape/exceptions/incompatible_option_values.rb b/lib/grape/exceptions/incompatible_option_values.rb index 804581ace7..5f42ea43a4 100644 --- a/lib/grape/exceptions/incompatible_option_values.rb +++ b/lib/grape/exceptions/incompatible_option_values.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class IncompatibleOptionValues < Base def initialize(option1, value1, option2, value2) - super(message: compose_message('incompatible_option_values', option1: option1, value1: value1, option2: option2, value2: value2)) + super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2)) end end end diff --git a/lib/grape/exceptions/invalid_accept_header.rb b/lib/grape/exceptions/invalid_accept_header.rb index 0141783709..5442016075 100644 --- a/lib/grape/exceptions/invalid_accept_header.rb +++ b/lib/grape/exceptions/invalid_accept_header.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidAcceptHeader < Base def initialize(message, headers) - super(message: compose_message('invalid_accept_header', message: message), status: 406, headers: headers) + super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers) end end end diff --git a/lib/grape/exceptions/invalid_formatter.rb b/lib/grape/exceptions/invalid_formatter.rb index 48754293df..5d069b91fd 100644 --- a/lib/grape/exceptions/invalid_formatter.rb +++ b/lib/grape/exceptions/invalid_formatter.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidFormatter < Base def initialize(klass, to_format) - super(message: compose_message('invalid_formatter', klass: klass, to_format: to_format)) + super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format)) end end end diff --git a/lib/grape/exceptions/invalid_message_body.rb b/lib/grape/exceptions/invalid_message_body.rb index f5866b1584..c0f05281ea 100644 --- a/lib/grape/exceptions/invalid_message_body.rb +++ b/lib/grape/exceptions/invalid_message_body.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidMessageBody < Base def initialize(body_format) - super(message: compose_message('invalid_message_body', body_format: body_format), status: 400) + super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400) end end end diff --git a/lib/grape/exceptions/invalid_version_header.rb b/lib/grape/exceptions/invalid_version_header.rb index 48f2307e51..6b93ea7e98 100644 --- a/lib/grape/exceptions/invalid_version_header.rb +++ b/lib/grape/exceptions/invalid_version_header.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidVersionHeader < Base def initialize(message, headers) - super(message: compose_message('invalid_version_header', message: message), status: 406, headers: headers) + super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers) end end end diff --git a/lib/grape/exceptions/invalid_versioner_option.rb b/lib/grape/exceptions/invalid_versioner_option.rb index e41ba03f5b..14aea16844 100644 --- a/lib/grape/exceptions/invalid_versioner_option.rb +++ b/lib/grape/exceptions/invalid_versioner_option.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidVersionerOption < Base def initialize(strategy) - super(message: compose_message('invalid_versioner_option', strategy: strategy)) + super(message: compose_message(:invalid_versioner_option, strategy: strategy)) end end end diff --git a/lib/grape/exceptions/invalid_with_option_for_represent.rb b/lib/grape/exceptions/invalid_with_option_for_represent.rb index 8c3c2067d7..521a1e0196 100644 --- a/lib/grape/exceptions/invalid_with_option_for_represent.rb +++ b/lib/grape/exceptions/invalid_with_option_for_represent.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class InvalidWithOptionForRepresent < Base def initialize - super(message: compose_message('invalid_with_option_for_represent')) + super(message: compose_message(:invalid_with_option_for_represent)) end end end diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index cb05fdccaf..31faa6887f 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class MissingGroupTypeError < Base def initialize - super(message: compose_message('missing_group_type')) + super(message: compose_message(:missing_group_type)) end end end diff --git a/lib/grape/exceptions/missing_mime_type.rb b/lib/grape/exceptions/missing_mime_type.rb index c6958d1bd6..faaef5b53a 100644 --- a/lib/grape/exceptions/missing_mime_type.rb +++ b/lib/grape/exceptions/missing_mime_type.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class MissingMimeType < Base def initialize(new_format) - super(message: compose_message('missing_mime_type', new_format: new_format)) + super(message: compose_message(:missing_mime_type, new_format: new_format)) end end end diff --git a/lib/grape/exceptions/missing_option.rb b/lib/grape/exceptions/missing_option.rb index 8a3a2e36b9..19d2aa61d3 100644 --- a/lib/grape/exceptions/missing_option.rb +++ b/lib/grape/exceptions/missing_option.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class MissingOption < Base def initialize(option) - super(message: compose_message('missing_option', option: option)) + super(message: compose_message(:missing_option, option: option)) end end end diff --git a/lib/grape/exceptions/missing_vendor_option.rb b/lib/grape/exceptions/missing_vendor_option.rb index adc6a38041..b30ec42c6e 100644 --- a/lib/grape/exceptions/missing_vendor_option.rb +++ b/lib/grape/exceptions/missing_vendor_option.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class MissingVendorOption < Base def initialize - super(message: compose_message('missing_vendor_option')) + super(message: compose_message(:missing_vendor_option)) end end end diff --git a/lib/grape/exceptions/unknown_options.rb b/lib/grape/exceptions/unknown_options.rb index a3063e43be..fc92fab799 100644 --- a/lib/grape/exceptions/unknown_options.rb +++ b/lib/grape/exceptions/unknown_options.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class UnknownOptions < Base def initialize(options) - super(message: compose_message('unknown_options', options: options)) + super(message: compose_message(:unknown_options, options: options)) end end end diff --git a/lib/grape/exceptions/unknown_parameter.rb b/lib/grape/exceptions/unknown_parameter.rb index ca861b221a..7ed3b19afd 100644 --- a/lib/grape/exceptions/unknown_parameter.rb +++ b/lib/grape/exceptions/unknown_parameter.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class UnknownParameter < Base def initialize(param) - super(message: compose_message('unknown_parameter', param: param)) + super(message: compose_message(:unknown_parameter, param: param)) end end end diff --git a/lib/grape/exceptions/unknown_validator.rb b/lib/grape/exceptions/unknown_validator.rb index 281377c99e..3a6bf4220f 100644 --- a/lib/grape/exceptions/unknown_validator.rb +++ b/lib/grape/exceptions/unknown_validator.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class UnknownValidator < Base def initialize(validator_type) - super(message: compose_message('unknown_validator', validator_type: validator_type)) + super(message: compose_message(:unknown_validator, validator_type: validator_type)) end end end diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index 1a09941e3a..2ebada66e9 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -3,7 +3,7 @@ module Grape module Exceptions class UnsupportedGroupTypeError < Base def initialize - super(message: compose_message('unsupported_group_type')) + super(message: compose_message(:unsupported_group_type)) end end end diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index def15de8b1..8c910875d1 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -9,8 +9,7 @@ class Validation < Grape::Exceptions::Base def initialize(args = {}) fail 'Params are missing:' unless args.key? :params @params = args[:params] - @message_key = args[:message_key] - args[:message] = translate_message(args[:message_key]) if args.key? :message_key + args[:message] = translate_message(args[:message]) if args.key? :message super end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 809b452b7f..7466ce5f3e 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -204,7 +204,7 @@ def validates(attrs, validations) default = validations[:default] doc_attrs[:default] = default if validations.key?(:default) - values = validations[:values] + values = (options_key?(:values, :value, validations)) ? validations[:values][:value] : validations[:values] doc_attrs[:values] = values if values coerce_type = guess_coerce_type(coerce_type, values) @@ -224,6 +224,7 @@ def validates(attrs, validations) if validations.key?(:presence) && validations[:presence] validate('presence', validations[:presence], attrs, doc_attrs) validations.delete(:presence) + validations.delete(:message) if validations.key?(:message) end # Before we run the rest of the validators, let's handle @@ -254,8 +255,12 @@ def infer_coercion(validations) fail ArgumentError, ':type may not be supplied with :types' end - validations[:coerce] = validations[:type] if validations.key?(:type) - validations[:coerce] = validations.delete(:types) if validations.key?(:types) + validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type) + validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type) + validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types) + validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types) + + validations.delete(:types) if validations.key?(:types) coerce_type = validations[:coerce] @@ -300,11 +305,13 @@ def coerce_type(validations, attrs, doc_attrs) coerce_options = { type: validations[:coerce], - method: validations[:coerce_with] + method: validations[:coerce_with], + message: validations[:coerce_message] } validate('coerce', coerce_options, attrs, doc_attrs) validations.delete(:coerce_with) validations.delete(:coerce) + validations.delete(:coerce_message) end def guess_coerce_type(coerce_type, values) @@ -342,6 +349,16 @@ def validate_value_coercion(coerce_type, values) return unless value_types.any? { |v| !v.is_a?(coerce_type) } fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) end + + def extract_message_option(attrs) + return nil unless attrs.is_a?(Array) + opts = attrs.last.is_a?(Hash) ? attrs.pop : {} + (opts.key?(:message) && !opts[:message].nil?) ? opts.delete(:message) : nil + end + + def options_key?(type, key, validations) + validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil? + end end end end diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none.rb index 41ded7b642..2c9b54037f 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none.rb @@ -5,7 +5,7 @@ class AllOrNoneOfValidator < MultipleParamsBase def validate!(params) super if scope_requires_params && only_subset_present - fail Grape::Exceptions::Validation, params: all_keys, message_key: :all_or_none + fail Grape::Exceptions::Validation, params: all_keys, message: message(:all_or_none) end params end diff --git a/lib/grape/validations/validators/allow_blank.rb b/lib/grape/validations/validators/allow_blank.rb index 288ea5a79d..e4216bf420 100644 --- a/lib/grape/validations/validators/allow_blank.rb +++ b/lib/grape/validations/validators/allow_blank.rb @@ -2,7 +2,7 @@ module Grape module Validations class AllowBlankValidator < Base def validate_param!(attr_name, params) - return if @option || !params.is_a?(Hash) + return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) value = params[attr_name] value = value.strip if value.respond_to?(:strip) @@ -23,7 +23,7 @@ def validate_param!(attr_name, params) return if value == false || value.present? - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :blank + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:blank) end end end diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of.rb index c46b27d75b..9e7f47b510 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of.rb @@ -5,7 +5,7 @@ class AtLeastOneOfValidator < MultipleParamsBase def validate!(params) super if scope_requires_params && no_exclusive_params_are_present - fail Grape::Exceptions::Validation, params: all_keys, message_key: :at_least_one + fail Grape::Exceptions::Validation, params: all_keys, message: message(:at_least_one) end params end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index eae0756131..35c5cfc5b9 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -55,6 +55,16 @@ def self.inherited(klass) short_name = convert_to_short_name(klass) Validations.register_validator(short_name, klass) end + + def message(default_key = nil) + options = instance_variable_get(:@option) + options_key?(:message) ? options[:message] : default_key + end + + def options_key?(key, options = nil) + options = instance_variable_get(:@option) if options.nil? + options.respond_to?(:key?) && options.key?(key) && !options[key].nil? + end end end end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index 6244807287..8980659ccb 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -11,12 +11,12 @@ def initialize(*_args) end def validate_param!(attr_name, params) - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :coerce unless params.is_a? Hash + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless params.is_a? Hash new_value = coerce_value(params[attr_name]) if valid_type?(new_value) params[attr_name] = new_value else - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :coerce + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) end end @@ -60,7 +60,7 @@ def coerce_value(val) # # @return [Class] def type - @option[:type] + @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end end end diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of.rb index f323dac77c..4b23cd8a00 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of.rb @@ -5,11 +5,20 @@ class ExactlyOneOfValidator < MutualExclusionValidator def validate!(params) super if scope_requires_params && none_of_restricted_params_is_present - fail Grape::Exceptions::Validation, params: all_keys, message_key: :exactly_one + fail Grape::Exceptions::Validation, params: all_keys, message: message(:exactly_one) end params end + def message(default_key = nil) + options = instance_variable_get(:@option) + if options_key?(:message) + (options_key?(default_key, options[:message]) ? options[:message][default_key] : options[:message]) + else + default_key + end + end + private def none_of_restricted_params_is_present diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion.rb index 9f6dada170..32bdd1f5a6 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion.rb @@ -7,7 +7,7 @@ class MutualExclusionValidator < MultipleParamsBase def validate!(params) super if two_or_more_exclusive_params_are_present - fail Grape::Exceptions::Validation, params: processing_keys_in_common, message_key: :mutual_exclusion + fail Grape::Exceptions::Validation, params: processing_keys_in_common, message: message(:mutual_exclusion) end params end diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence.rb index 070ca68759..cc11c4c4b3 100644 --- a/lib/grape/validations/validators/presence.rb +++ b/lib/grape/validations/validators/presence.rb @@ -8,7 +8,7 @@ def validate!(params) def validate_param!(attr_name, params) return if params.respond_to?(:key?) && params.key?(attr_name) - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :presence + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:presence) end end end diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp.rb index e92e6f38c7..8e5c78f63f 100644 --- a/lib/grape/validations/validators/regexp.rb +++ b/lib/grape/validations/validators/regexp.rb @@ -2,8 +2,8 @@ module Grape module Validations class RegexpValidator < Base def validate_param!(attr_name, params) - return unless params.key?(attr_name) && !params[attr_name].nil? && !(params[attr_name].to_s =~ @option) - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :regexp + return unless params.key?(attr_name) && !params[attr_name].nil? && !(params[attr_name].to_s =~ (options_key?(:value) ? @option[:value] : @option)) + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:regexp) end end end diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values.rb index d7aebaeeb9..26f11cec6e 100644 --- a/lib/grape/validations/validators/values.rb +++ b/lib/grape/validations/validators/values.rb @@ -2,7 +2,7 @@ module Grape module Validations class ValuesValidator < Base def initialize(attrs, options, required, scope) - @values = options + @values = (options_key?(:value, options) ? options[:value] : options) super end @@ -13,7 +13,7 @@ def validate_param!(attr_name, params) values = @values.is_a?(Proc) ? @values.call : @values param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) return if param_array.all? { |param| values.include?(param) } - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :values + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) end private diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 9511bd2826..d190615bb4 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -30,6 +30,12 @@ def validates(*args) def validates_reader @validates end + + def extract_message_option(attrs) + return nil unless attrs.is_a?(Array) + opts = attrs.last.is_a?(Hash) ? attrs.pop : {} + (opts.key?(:message) && !opts[:message].nil?) ? opts.delete(:message) : nil + end end end @@ -72,7 +78,7 @@ def validates_reader it 'adds a required parameter' do subject.requires :id, type: Integer, desc: 'Identity.' - expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: true }]) + expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }]) expect(subject.push_declared_params_reader).to eq([[:id]]) end end @@ -90,7 +96,7 @@ def validates_reader it 'adds an mutally exclusive parameter validation' do subject.mutually_exclusive :media, :audio - expect(subject.validates_reader).to eq([[:media, :audio], { mutual_exclusion: true }]) + expect(subject.validates_reader).to eq([[:media, :audio], { mutual_exclusion: { value: true, message: nil } }]) end end @@ -98,7 +104,7 @@ def validates_reader it 'adds an exactly of one parameter validation' do subject.exactly_one_of :media, :audio - expect(subject.validates_reader).to eq([[:media, :audio], { exactly_one_of: true }]) + expect(subject.validates_reader).to eq([[:media, :audio], { exactly_one_of: { value: true, message: nil } }]) end end @@ -106,7 +112,7 @@ def validates_reader it 'adds an at least one of parameter validation' do subject.at_least_one_of :media, :audio - expect(subject.validates_reader).to eq([[:media, :audio], { at_least_one_of: true }]) + expect(subject.validates_reader).to eq([[:media, :audio], { at_least_one_of: { value: true, message: nil } }]) end end @@ -114,7 +120,7 @@ def validates_reader it 'adds an all or none of parameter validation' do subject.all_or_none_of :media, :audio - expect(subject.validates_reader).to eq([[:media, :audio], { all_or_none_of: true }]) + expect(subject.validates_reader).to eq([[:media, :audio], { all_or_none_of: { value: true, message: nil } }]) end end diff --git a/spec/grape/exceptions/validation_errors_spec.rb b/spec/grape/exceptions/validation_errors_spec.rb index 31b1ef0c31..b29b52cd99 100644 --- a/spec/grape/exceptions/validation_errors_spec.rb +++ b/spec/grape/exceptions/validation_errors_spec.rb @@ -35,8 +35,8 @@ describe '#full_messages' do context 'with errors' do - let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message_key: 'presence') } - let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message_key: 'presence') } + let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message: :presence) } + let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message: :presence) } subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages } it 'returns an array with each errors full message' do diff --git a/spec/grape/exceptions/validation_spec.rb b/spec/grape/exceptions/validation_spec.rb index f0c02450f1..1feae1e20c 100644 --- a/spec/grape/exceptions/validation_spec.rb +++ b/spec/grape/exceptions/validation_spec.rb @@ -2,10 +2,6 @@ describe Grape::Exceptions::Validation do it 'fails when params are missing' do - expect { Grape::Exceptions::Validation.new(message_key: 'presence') }.to raise_error(RuntimeError, 'Params are missing:') - end - - it 'store message_key' do - expect(Grape::Exceptions::Validation.new(params: ['id'], message_key: 'presence').message_key).to eq('presence') + expect { Grape::Exceptions::Validation.new(message: 'presence') }.to raise_error(RuntimeError, 'Params are missing:') end end diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index a400913e22..144b774ccb 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -120,6 +120,123 @@ class API < Grape::API end end get '/disallow_string_value_in_an_optional_hash_group' + + resources :custom_message do + params do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + get + + params do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + get '/disallow_blank_optional_param' + + params do + requires :name, allow_blank: true + end + get '/allow_blank' + + params do + requires :val, type: DateTime, allow_blank: true + end + get '/allow_datetime_blank' + + params do + requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } + end + get '/disallow_datetime_blank' + + params do + requires :val, type: DateTime + end + get '/default_allow_datetime_blank' + + params do + requires :val, type: Date, allow_blank: true + end + get '/allow_date_blank' + + params do + requires :val, type: Integer, allow_blank: true + end + get '/allow_integer_blank' + + params do + requires :val, type: Float, allow_blank: true + end + get '/allow_float_blank' + + params do + requires :val, type: Fixnum, allow_blank: true + end + get '/allow_fixnum_blank' + + params do + requires :val, type: Symbol, allow_blank: true + end + get '/allow_symbol_blank' + + params do + requires :val, type: Boolean, allow_blank: true + end + get '/allow_boolean_blank' + + params do + requires :val, type: Boolean, allow_blank: { value: false, message: 'has no value' } + end + get '/disallow_boolean_blank' + + params do + optional :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/disallow_blank_required_param_in_an_optional_group' + + params do + optional :user, type: Hash do + requires :name, type: Date, allow_blank: true + end + end + get '/allow_blank_date_param_in_an_optional_group' + + params do + optional :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + requires :age + end + end + get '/disallow_blank_optional_param_in_an_optional_group' + + params do + requires :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/disallow_blank_required_param_in_a_required_group' + + params do + requires :user, type: Hash do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/disallow_string_value_in_a_required_hash_group' + + params do + requires :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/disallow_blank_optional_param_in_a_required_group' + + params do + optional :user, type: Hash do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + end + get '/disallow_string_value_in_an_optional_hash_group' + end end end end @@ -154,6 +271,166 @@ def app end end + context 'custom validation message' do + context 'with invalid input' do + it 'refuses empty string' do + get '/custom_message', name: '' + expect(last_response.body).to eq('{"error":"name has no value"}') + end + it 'refuses empty string for an optional param' do + get '/custom_message/disallow_blank_optional_param', name: '' + expect(last_response.body).to eq('{"error":"name has no value"}') + end + it 'refuses only whitespaces' do + get '/custom_message', name: ' ' + expect(last_response.body).to eq('{"error":"name has no value"}') + + get '/custom_message', name: " \n " + expect(last_response.body).to eq('{"error":"name has no value"}') + + get '/custom_message', name: "\n" + expect(last_response.body).to eq('{"error":"name has no value"}') + end + + it 'refuses nil' do + get '/custom_message', name: nil + expect(last_response.body).to eq('{"error":"name has no value"}') + end + end + + context 'with valid input' do + it 'accepts valid input' do + get '/custom_message', name: 'bob' + expect(last_response.status).to eq(200) + end + + it 'accepts empty input when allow_blank is false' do + get '/custom_message/allow_blank', name: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty input' do + get '/custom_message/default_allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when datetime allow_blank' do + get '/custom_message/allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when date allow_blank' do + get '/custom_message/allow_date_blank', val: '' + expect(last_response.status).to eq(200) + end + + context 'allow_blank when Numeric' do + it 'accepts empty when integer allow_blank' do + get '/custom_message/allow_integer_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when float allow_blank' do + get '/custom_message/allow_float_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when fixnum allow_blank' do + get '/custom_message/allow_fixnum_blank', val: '' + expect(last_response.status).to eq(200) + end + end + + it 'accepts empty when symbol allow_blank' do + get '/custom_message/allow_symbol_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when boolean allow_blank' do + get '/custom_message/allow_boolean_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts false when boolean allow_blank' do + get '/custom_message/disallow_boolean_blank', val: false + expect(last_response.status).to eq(200) + end + end + + context 'in an optional group' do + context 'as a required param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/custom_message/disallow_blank_required_param_in_an_optional_group' + expect(last_response.status).to eq(200) + end + + it 'accepts a nested missing date value' do + get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(200) + end + + it 'refuses a blank value in an existing group' do + get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user[name] has no value"}') + end + end + + context 'as an optional param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/custom_message/disallow_blank_optional_param_in_an_optional_group' + expect(last_response.status).to eq(200) + end + + it 'accepts a nested missing optional value' do + get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } + expect(last_response.status).to eq(200) + end + + it 'refuses a blank existing value in an existing scope' do + get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user[name] has no value"}') + end + end + end + + context 'in a required group' do + context 'as a required param' do + it 'refuses a blank value in a required existing group' do + get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user[name] has no value"}') + end + + it 'refuses a string value in a required hash group' do + get '/custom_message/disallow_string_value_in_a_required_hash_group', user: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}') + end + end + + context 'as an optional param' do + it 'accepts a nested missing value' do + get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } + expect(last_response.status).to eq(200) + end + + it 'refuses a blank existing value in an existing scope' do + get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user[name] has no value"}') + end + + it 'refuses a string value in an optional hash group' do + get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"user is invalid"}') + end + end + end + end + context 'valid input' do it 'accepts valid input' do get '/', name: 'bob' diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index ffa89e855d..f0cb2d41d3 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -55,6 +55,62 @@ class User end end + context 'with a custom validation message' do + it 'errors on malformed input' do + subject.params do + requires :int, type: { value: Integer, message: 'type cast is invalid' } + end + subject.get '/single' do + 'int works' + end + + get '/single', int: '43a' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('int type cast is invalid') + + get '/single', int: '43' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('int works') + end + + context 'on custom coercion rules' do + before do + subject.params do + requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| + if val == 'yup' + true + elsif val == 'false' + 0 + else + val + end + end) + end + subject.get '/' do + params[:a].class.to_s + end + end + + it 'respects :coerce_with' do + get '/', a: 'yup' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('TrueClass') + end + + it 'still validates type' do + get '/', a: 'false' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('a type cast is invalid') + end + + it 'performs no additional coercion' do + get '/', a: 'true' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('String') + end + end + end + it 'error on malformed input' do subject.params do requires :int, type: Integer diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index 980bec1d8c..6f287b69c3 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -25,6 +25,33 @@ def app end end + context 'with a custom validation message' do + before do + subject.resource :requires do + params do + requires :email, type: String, allow_blank: { value: false, message: 'has no value' }, regexp: { value: /^\S+$/, message: 'format is invalid' }, message: 'is required' + end + get do + 'Hello' + end + end + end + it 'requires when missing' do + get '/requires' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"email is required, email has no value"}') + end + it 'requires when empty' do + get '/requires', email: '' + expect(last_response.body).to eq('{"error":"email has no value, email format is invalid"}') + end + it 'valid when set' do + get '/requires', email: 'bob@example.com' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('Hello'.to_json) + end + end + context 'with a required regexp parameter supplied in the POST body' do before do subject.format :json diff --git a/spec/grape/validations/validators/regexp_spec.rb b/spec/grape/validations/validators/regexp_spec.rb index 5c5b353037..659bc61d29 100644 --- a/spec/grape/validations/validators/regexp_spec.rb +++ b/spec/grape/validations/validators/regexp_spec.rb @@ -6,6 +6,14 @@ module RegexpValidatorSpec class API < Grape::API default_format :json + resources :custom_message do + params do + requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + end + get do + end + end + params do requires :name, regexp: /^[a-z]+$/ end @@ -19,15 +27,43 @@ def app ValidationsSpec::RegexpValidatorSpec::API end + context 'custom validation message' do + context 'with invalid input' do + it 'refuses inapppopriate' do + get '/custom_message', name: 'invalid name' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name format is invalid"}') + end + + it 'refuses empty' do + get '/custom_message', name: '' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name format is invalid"}') + end + end + + it 'accepts nil' do + get '/custom_message', name: nil + expect(last_response.status).to eq(200) + end + + it 'accepts valid input' do + get '/custom_message', name: 'bob' + expect(last_response.status).to eq(200) + end + end + context 'invalid input' do it 'refuses inapppopriate' do get '/', name: 'invalid name' expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name is invalid"}') end it 'refuses empty' do get '/', name: '' expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"name is invalid"}') end end diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index fff05de2a4..064dffc544 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -21,6 +21,22 @@ module ValuesValidatorSpec class API < Grape::API default_format :json + resources :custom_message do + params do + requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } + end + get '/' do + { type: params[:type] } + end + + params do + optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' + end + get '/lambda' do + { type: params[:type] } + end + end + params do requires :type, values: ValuesModel.values end @@ -91,6 +107,34 @@ def app ValidationsSpec::ValuesValidatorSpec::API end + context 'with a custom validation message' do + it 'allows a valid value for a parameter' do + get('/custom_message', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + end + + it 'does not allow an invalid value for a parameter' do + get('/custom_message', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + end + + it 'validates against values in a proc' do + ValidationsSpec::ValuesModel.add_value('valid-type4') + + get('/custom_message/lambda', type: 'valid-type4') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) + end + + it 'does not allow an invalid value for a parameter using lambda' do + get('/custom_message/lambda', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) + end + end + it 'allows a valid value for a parameter' do get('/', type: 'valid-type1') expect(last_response.status).to eq 200 diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index b07272df9d..f82f43bf66 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -942,14 +942,14 @@ module CustomValidations class CustomvalidatorWithOptions < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name] == @option[:text] - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message] + fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message end end end before do subject.params do - optional :custom, customvalidator_with_options: { text: 'im custom with options', error_message: 'is not custom with options!' } + optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } end subject.get '/optional_custom' do 'optional with custom works!' @@ -1078,8 +1078,62 @@ def validate_param!(attr_name, params) end end + context 'all or none' do + context 'optional params' do + before :each do + subject.resource :custom_message do + params do + optional :beer + optional :wine + optional :juice + all_or_none_of :beer, :wine, :juice, message: 'all params are required or none is required' + end + get '/all_or_none' do + 'all_or_none works!' + end + end + end + context 'with a custom validation message' do + it 'errors when any one is present' do + get '/custom_message/all_or_none', beer: 'string' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine, juice all params are required or none is required' + end + it 'works when all params are present' do + get '/custom_message/all_or_none', beer: 'string', wine: 'anotherstring', juice: 'anotheranotherstring' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'all_or_none works!' + end + it 'works when none are present' do + get '/custom_message/all_or_none' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'all_or_none works!' + end + end + end + end + context 'mutually exclusive' do context 'optional params' do + context 'with custom validation message' do + it 'errors when two or more are present' do + subject.resources :custom_message do + params do + optional :beer + optional :wine + optional :juice + mutually_exclusive :beer, :wine, :juice, message: 'are mutually exclusive cannot pass both params' + end + get '/mutually_exclusive' do + 'mutually_exclusive works!' + end + end + get '/custom_message/mutually_exclusive', beer: 'string', wine: 'anotherstring' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine are mutually exclusive cannot pass both params' + end + end + it 'errors when two or more are present' do subject.params do optional :beer @@ -1098,6 +1152,34 @@ def validate_param!(attr_name, params) end context 'more than one set of mutually exclusive params' do + context 'with a custom validation message' do + it 'errors for all sets' do + subject.resources :custom_message do + params do + optional :beer + optional :wine + mutually_exclusive :beer, :wine, message: 'are mutually exclusive pass only one' + optional :nested, type: Hash do + optional :scotch + optional :aquavit + mutually_exclusive :scotch, :aquavit, message: 'are mutually exclusive pass only one' + end + optional :nested2, type: Array do + optional :scotch2 + optional :aquavit2 + mutually_exclusive :scotch2, :aquavit2, message: 'are mutually exclusive pass only one' + end + end + get '/mutually_exclusive' do + 'mutually_exclusive works!' + end + end + get '/custom_message/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine are mutually exclusive pass only one, scotch, aquavit are mutually exclusive pass only one, scotch2, aquavit2 are mutually exclusive pass only one' + end + end + it 'errors for all sets' do subject.params do optional :beer @@ -1131,7 +1213,6 @@ def validate_param!(attr_name, params) optional :wine optional :beer optional :juice - mutually_exclusive :beer, :wine, :juice end end @@ -1185,6 +1266,18 @@ def validate_param!(attr_name, params) context 'exactly one of' do context 'params' do before :each do + subject.resources :custom_message do + params do + optional :beer + optional :wine + optional :juice + exactly_one_of :beer, :wine, :juice, message: { exactly_one: 'are missing, exactly one parameter is required', mutual_exclusion: 'are mutually exclusive, exactly one parameter is required' } + end + get '/exactly_one_of' do + 'exactly_one_of works!' + end + end + subject.params do optional :beer optional :wine @@ -1196,6 +1289,26 @@ def validate_param!(attr_name, params) end end + context 'with a custom validation message' do + it 'errors when none are present' do + get '/custom_message/exactly_one_of' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter is required' + end + + it 'succeeds when one is present' do + get '/custom_message/exactly_one_of', beer: 'string' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'exactly_one_of works!' + end + + it 'errors when two or more are present' do + get '/custom_message/exactly_one_of', beer: 'string', wine: 'anotherstring' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine are mutually exclusive, exactly one parameter is required' + end + end + it 'errors when none are present' do get '/exactly_one_of' expect(last_response.status).to eq(400) @@ -1259,6 +1372,18 @@ def validate_param!(attr_name, params) context 'at least one of' do context 'params' do before :each do + subject.resources :custom_message do + params do + optional :beer + optional :wine + optional :juice + at_least_one_of :beer, :wine, :juice, message: 'are missing, please specify at least one param' + end + get '/at_least_one_of' do + 'at_least_one_of works!' + end + end + subject.params do optional :beer optional :wine @@ -1270,6 +1395,26 @@ def validate_param!(attr_name, params) end end + context 'with a custom validation message' do + it 'errors when none are present' do + get '/custom_message/at_least_one_of' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq 'beer, wine, juice are missing, please specify at least one param' + end + + it 'does not error when one is present' do + get '/custom_message/at_least_one_of', beer: 'string' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'at_least_one_of works!' + end + + it 'does not error when two are present' do + get '/custom_message/at_least_one_of', beer: 'string', wine: 'string' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq 'at_least_one_of works!' + end + end + it 'errors when none are present' do get '/at_least_one_of' expect(last_response.status).to eq(400)