From 1e888c89db89d5132911c44c5ef72aa4af661101 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Thu, 12 Feb 2026 09:41:16 +0100 Subject: [PATCH] Instantiate validators at definition time - Store validator instances in ParamsScope/ContractScope and have Endpoint#run_validators read them directly - Remove ValidatorFactory indirection and eagerly compute validator messages/options in constructors - Extract Grape::Util::Translation module shared by Exceptions::Base and Validators::Base for I18n translate with fallback locale - Support Hash messages in translate_message for deferred translation with interpolation parameters (e.g. { key: :length, min: 2 }) - Normalize Grape::Exceptions::Validation params handling and refactor validator specs to define routes per example group - Drop test-prof dependency and its spec config Co-authored-by: Cursor --- CHANGELOG.md | 1 + Gemfile | 1 - UPGRADING.md | 86 ++ lib/grape/endpoint.rb | 19 +- lib/grape/exceptions/base.rb | 49 +- lib/grape/exceptions/validation.rb | 4 +- lib/grape/util/translation.rb | 22 + lib/grape/validations/contract_scope.rb | 8 +- lib/grape/validations/params_scope.rb | 25 +- lib/grape/validations/validator_factory.rb | 15 - .../validators/all_or_none_of_validator.rb | 7 +- .../validators/allow_blank_validator.rb | 15 +- .../validators/at_least_one_of_validator.rb | 9 +- lib/grape/validations/validators/base.rb | 48 +- .../validators/coerce_validator.rb | 42 +- .../validators/contract_scope_validator.rb | 17 +- .../validators/default_validator.rb | 28 +- .../validators/exactly_one_of_validator.rb | 10 +- .../validators/except_values_validator.rb | 21 +- .../validators/length_validator.rb | 40 +- .../mutually_exclusive_validator.rb | 7 +- .../validators/presence_validator.rb | 9 +- .../validators/regexp_validator.rb | 19 +- .../validators/same_as_validator.rb | 22 +- .../validators/values_validator.rb | 28 +- spec/config/spec_test_prof.rb | 9 - spec/grape/endpoint_spec.rb | 6 - .../validators/all_or_none_validator_spec.rb | 198 +++-- .../validators/allow_blank_validator_spec.rb | 625 +++++++------- .../at_least_one_of_validator_spec.rb | 215 +++-- .../validations/validators/base_i18n_spec.rb | 75 ++ .../contract_scope_validator_spec.rb | 9 - .../validators/default_validator_spec.rb | 388 +++++---- .../exactly_one_of_validator_spec.rb | 276 +++--- .../validators/length_validator_spec.rb | 288 ++++--- .../validators/mutually_exclusive_spec.rb | 254 +++--- .../validators/regexp_validator_spec.rb | 202 +++-- .../validators/same_as_validator_spec.rb | 36 +- .../validators/values_validator_spec.rb | 797 +++++++++++------- spec/grape/validations/validators/zh-CN.yml | 1 + spec/spec_helper.rb | 2 +- 41 files changed, 2312 insertions(+), 1621 deletions(-) create mode 100644 lib/grape/util/translation.rb delete mode 100644 lib/grape/validations/validator_factory.rb delete mode 100644 spec/config/spec_test_prof.rb create mode 100644 spec/grape/validations/validators/base_i18n_spec.rb delete mode 100644 spec/grape/validations/validators/contract_scope_validator_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d009aa085..97f86f880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Features * [#2656](https://github.com/ruby-grape/grape/pull/2656): Remove useless instance_variable_defined? checks - [@ericproulx](https://github.com/ericproulx). +* [#2657](https://github.com/ruby-grape/grape/pull/2657): Instantiate validators at compile time - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/Gemfile b/Gemfile index abf49f83f..0eb788a1d 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,6 @@ group :test do gem 'rspec', '~> 3.13' gem 'simplecov', '~> 0.21', require: false gem 'simplecov-lcov', '~> 0.8', require: false - gem 'test-prof', require: false end platforms :jruby do diff --git a/UPGRADING.md b/UPGRADING.md index 30ea387e5..0ededd98d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,92 @@ Upgrading Grape =============== +### Upgrading to >= 3.2 + +#### Validators Instantiated at Definition Time + +Previously, validators were instantiated at request time but they are now instantiated at definition time. This reduces object allocations since instances are reused across requests. + +#### `Grape::Util::Translation` Module + +I18n translation logic (translate with fallback locale) has been extracted into `Grape::Util::Translation`, included by both `Grape::Exceptions::Base` and `Grape::Validations::Validators::Base`. The `FALLBACK_LOCALE` constant has moved from `Grape::Exceptions::Base` to `Grape::Util::Translation`: + +#### `Grape::Exceptions::Base#translate_message` Supports Hash Messages + +`translate_message` now accepts a Hash with a `:key` and interpolation parameters for deferred I18n translation: + +```ruby +# Symbol (unchanged) +translate_message(:presence) + +# Hash (new) — key + interpolation params, translated at error-raise time +translate_message({ key: :length, min: 2, max: 5 }) +``` + +This is used by validators that need locale-sensitive messages with interpolation (e.g. `LengthValidator`, `SameAsValidator`). + +#### `Grape::Exceptions::Validation` Changes + +**`params` and `message_key` are now read-only.** `attr_accessor` has been changed to `attr_reader`. If you were assigning to these after initialization, set them via the constructor keyword arguments instead. + +**`params` is now always coerced to an array.** You can now pass a single string instead of wrapping it in an array: + +```ruby +# Before +Grape::Exceptions::Validation.new(params: ['my_param'], message: 'is invalid') + +# After (both work, single string is now accepted) +Grape::Exceptions::Validation.new(params: 'my_param', message: 'is invalid') +Grape::Exceptions::Validation.new(params: ['my_param'], message: 'is invalid') +``` + +#### `Validators::Base` Method Visibility Changes + +The following methods on `Grape::Validations::Validators::Base` are now **private**: `validate!`, `message`, `options_key?`. If your custom validator subclass calls these via `super` from a private method, no change is needed. If you were calling them from outside the class, you'll need to adjust. + +New private helpers have been added: +- `hash_like?(obj)` — returns `obj.respond_to?(:key?)` +- `option_value` — returns `@option[:value]` if present, otherwise `@option` +- `scrub(value)` — scrubs invalid-encoding strings +- `translate_message(key, **)` — translates a message key using the `grape.errors.messages` I18n scope with fallback locale support + +#### `Validators::Base#message` Now Accepts a Block + +`message` now accepts an optional block for lazy default message generation. When no custom `:message` option is set and no `default_key` is provided, the block is called: + +```ruby +# Before +def message(default_key = nil) + options_key?(:message) ? @option[:message] : default_key +end + +# After +def message(default_key = nil) + key = options_key?(:message) ? @option[:message] : default_key + return key if key + + yield if block_given? +end +``` + +If your custom validator overrides `message` or passes a `default_key`, the behavior is unchanged. If you relied on `message` returning `nil` when no custom message and no default key were set, it now yields to the block instead. + +#### `ContractScopeValidator` No Longer Inherits from `Base` + +`ContractScopeValidator` is now a standalone class that no longer inherits from `Grape::Validations::Validators::Base`. Its constructor takes a single `schema:` keyword argument instead of the standard 5-argument validator signature: + +```ruby +# Before +ContractScopeValidator.new(attrs, options, required, scope, opts) + +# After +ContractScopeValidator.new(schema: contract) +``` + +#### Validator Constructor Caching + +All built-in validators now eagerly compute and cache values in their constructors (exception messages, option values, lambdas for proc-based defaults/values). This is transparent to API consumers but relevant if you subclass built-in validators and override `initialize` — ensure you call `super` so caching is properly set up. + ### Upgrading to >= 3.1 #### Explicit kwargs for `namespace` and `route_param` diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index c8361faac..4e972d542 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -176,7 +176,7 @@ def run status 204 else run_filters before_validations, :before_validation - run_validators validations, request + run_validators request: request run_filters after_validations, :after_validation response_object = execute end @@ -205,10 +205,13 @@ def execute end end - def run_validators(validators, request) + def run_validators(request:) + validators = inheritable_setting.route[:saved_validations] + return if validators.blank? + validation_errors = [] - ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do + ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request:) do validators.each do |validator| validator.validate(request) rescue Grape::Exceptions::Validation => e @@ -237,16 +240,6 @@ def run_filters(filters, type = :other) end end - def validations - saved_validations = inheritable_setting.route[:saved_validations] - return if saved_validations.nil? - return enum_for(:validations) unless block_given? - - saved_validations.each do |saved_validation| - yield Grape::Validations::ValidatorFactory.create_validator(saved_validation) - end - end - def options? options[:options_route_enabled] && env[Rack::REQUEST_METHOD] == Rack::OPTIONS diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index be24e43ed..a00f27ec7 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -3,9 +3,7 @@ module Grape module Exceptions class Base < StandardError - BASE_MESSAGES_KEY = 'grape.errors.messages' - BASE_ATTRIBUTES_KEY = 'grape.errors.attributes' - FALLBACK_LOCALE = :en + include Grape::Util::Translation attr_reader :status, :headers @@ -25,50 +23,39 @@ def [](index) # TODO: translate attribute first # if BASE_ATTRIBUTES_KEY.key respond to a string message, then short_message is returned # if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution - def compose_message(key, **attributes) - short_message = translate_message(key, attributes) + def compose_message(key, **) + short_message = translate_message(key, **) return short_message unless short_message.is_a?(Hash) - each_steps(key, attributes).with_object(+'') do |detail_array, message| + each_steps(key, **).with_object(+'') do |detail_array, message| message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank? end end - def each_steps(key, attributes) - return enum_for(:each_steps, key, attributes) unless block_given? + def each_steps(key, **) + return enum_for(:each_steps, key, **) unless block_given? - yield 'Problem', translate_message(:"#{key}.problem", attributes) - yield 'Summary', translate_message(:"#{key}.summary", attributes) - yield 'Resolution', translate_message(:"#{key}.resolution", attributes) + yield 'Problem', translate_message(:"#{key}.problem", **) + yield 'Summary', translate_message(:"#{key}.summary", **) + yield 'Resolution', translate_message(:"#{key}.resolution", **) end - def translate_attributes(keys, options = {}) + def translate_attributes(keys, **) keys.map do |key| - translate("#{BASE_ATTRIBUTES_KEY}.#{key}", options.merge(default: key.to_s)) + translate(key, scope: 'grape.errors.attributes', default: key.to_s, **) end.join(', ') end - def translate_message(key, options = {}) - case key + def translate_message(translation_key, **) + case translation_key when Symbol - translate("#{BASE_MESSAGES_KEY}.#{key}", options.merge(default: '')) + translate(translation_key, scope: 'grape.errors.messages', **) + when Hash + translate(translation_key[:key], scope: 'grape.errors.messages', **translation_key.except(:key)) when Proc - key.call + translation_key.call else - key - end - end - - def translate(key, options) - message = ::I18n.translate(key, **options) - message.presence || fallback_message(key, options) - end - - def fallback_message(key, options) - if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE) - key - else - ::I18n.translate(key, locale: FALLBACK_LOCALE, **options) + translation_key end end end diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index 0a9e9c5dd..d962cb91b 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -3,10 +3,10 @@ module Grape module Exceptions class Validation < Base - attr_accessor :params, :message_key + attr_reader :params, :message_key def initialize(params:, message: nil, status: nil, headers: nil) - @params = params + @params = params.is_a?(Array) ? params : [params] if message @message_key = message if message.is_a?(Symbol) message = translate_message(message) diff --git a/lib/grape/util/translation.rb b/lib/grape/util/translation.rb new file mode 100644 index 000000000..272720119 --- /dev/null +++ b/lib/grape/util/translation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Grape + module Util + module Translation + FALLBACK_LOCALE = :en + + private + + def translate(key, default: '', scope: nil, **) + message = ::I18n.translate(key, default:, scope:, **) + return message if message.present? + + if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE) + scope ? "#{scope}.#{key}" : key + else + ::I18n.translate(key, default:, scope:, locale: FALLBACK_LOCALE, **) + end + end + end + end +end diff --git a/lib/grape/validations/contract_scope.rb b/lib/grape/validations/contract_scope.rb index b3ccd4ae9..07db7132d 100644 --- a/lib/grape/validations/contract_scope.rb +++ b/lib/grape/validations/contract_scope.rb @@ -21,13 +21,7 @@ def initialize(api, contract = nil, &block) end api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map - - validator_options = { - validator_class: Grape::Validations.require_validator(:contract_scope), - opts: { schema: contract, fail_fast: false } - } - - api.inheritable_setting.namespace_stackable[:validations] = validator_options + api.inheritable_setting.namespace_stackable[:validations] = Validators::ContractScopeValidator.new(schema: contract) end end end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 612d65f05..b2e60ab0d 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -357,7 +357,7 @@ def validates(attrs, validations) # Before we run the rest of the validators, let's handle # whatever coercion so that we are working with correctly # type casted values - coerce_type validations, attrs, required, opts + coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts validations.each do |type, options| # Don't try to look up validators for documentation params that don't have one. @@ -430,7 +430,7 @@ def check_coerce_with(validations) def coerce_type(validations, attrs, required, opts) check_coerce_with(validations) - return unless validations.key?(:coerce) + return unless validations[:coerce] coerce_options = { type: validations[:coerce], @@ -438,9 +438,6 @@ def coerce_type(validations, attrs, required, opts) message: validations[:coerce_message] } validate('coerce', coerce_options, attrs, required, opts) - validations.delete(:coerce_with) - validations.delete(:coerce) - validations.delete(:coerce_message) end def guess_coerce_type(coerce_type, *values_list) @@ -464,15 +461,15 @@ def check_incompatible_option_values(default, values, except_values) end def validate(type, options, attrs, required, opts) - validator_options = { - attributes: attrs, - options: options, - required: required, - params_scope: self, - opts: opts, - validator_class: Validations.require_validator(type) - } - @api.inheritable_setting.namespace_stackable[:validations] = validator_options + validator_class = Validations.require_validator(type) + validator_instance = validator_class.new( + attrs, + options, + required, + self, + opts + ) + @api.inheritable_setting.namespace_stackable[:validations] = validator_instance end def validate_value_coercion(coerce_type, *values_list) diff --git a/lib/grape/validations/validator_factory.rb b/lib/grape/validations/validator_factory.rb deleted file mode 100644 index 0e2022d3a..000000000 --- a/lib/grape/validations/validator_factory.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Grape - module Validations - class ValidatorFactory - def self.create_validator(options) - options[:validator_class].new(options[:attributes], - options[:options], - options[:required], - options[:params_scope], - options[:opts]) - end - end - end -end diff --git a/lib/grape/validations/validators/all_or_none_of_validator.rb b/lib/grape/validations/validators/all_or_none_of_validator.rb index 2fe553a15..668910f98 100644 --- a/lib/grape/validations/validators/all_or_none_of_validator.rb +++ b/lib/grape/validations/validators/all_or_none_of_validator.rb @@ -4,11 +4,16 @@ module Grape module Validations module Validators class AllOrNoneOfValidator < MultipleParamsBase + def initialize(attrs, options, required, scope, opts) + super + @exception_message = message(:all_or_none) + end + def validate_params!(params) keys = keys_in_common(params) return if keys.empty? || keys.length == all_keys.length - raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) + raise Grape::Exceptions::Validation.new(params: all_keys, message: @exception_message) end end end diff --git a/lib/grape/validations/validators/allow_blank_validator.rb b/lib/grape/validations/validators/allow_blank_validator.rb index e631fa2b0..540bdd740 100644 --- a/lib/grape/validations/validators/allow_blank_validator.rb +++ b/lib/grape/validations/validators/allow_blank_validator.rb @@ -4,15 +4,20 @@ module Grape module Validations module Validators class AllowBlankValidator < Base - def validate_param!(attr_name, params) - return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) + def initialize(attrs, options, required, scope, opts) + super + + @value = option_value + @exception_message = message(:blank) + end - value = params[attr_name] - value = value.scrub if value.respond_to?(:valid_encoding?) && !value.valid_encoding? + def validate_param!(attr_name, params) + return if @value || !hash_like?(params) + value = scrub(params[attr_name]) return if value == false || value.present? - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank)) + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end end end diff --git a/lib/grape/validations/validators/at_least_one_of_validator.rb b/lib/grape/validations/validators/at_least_one_of_validator.rb index 3467e4f1d..dda9c746a 100644 --- a/lib/grape/validations/validators/at_least_one_of_validator.rb +++ b/lib/grape/validations/validators/at_least_one_of_validator.rb @@ -4,10 +4,15 @@ module Grape module Validations module Validators class AtLeastOneOfValidator < MultipleParamsBase + def initialize(attrs, options, required, scope, opts) + super + @exception_message = message(:at_least_one) + end + def validate_params!(params) - return unless keys_in_common(params).empty? + return if keys_in_common(params).any? - raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) + raise Grape::Exceptions::Validation.new(params: all_keys, message: @exception_message) end end end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index f4788afb2..810c73b3e 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -4,6 +4,8 @@ module Grape module Validations module Validators class Base + include Grape::Util::Translation + attr_reader :attrs # Creates a new Validator from options specified @@ -19,8 +21,7 @@ def initialize(attrs, options, required, scope, opts) @option = options @required = required @scope = scope - @fail_fast = opts[:fail_fast] - @allow_blank = opts[:allow_blank] + @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank) end # Validates a given request. @@ -34,6 +35,17 @@ def validate(request) validate!(request.params) end + def self.inherited(klass) + super + Validations.register(klass) + end + + def fail_fast? + @fail_fast + end + + private + # Validates a given parameter hash. # @note Override #validate if you need to access the entire request. # @param params [Hash] parameters to validate @@ -49,7 +61,7 @@ def validate!(params) next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) - validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) + validate_param!(attr_name, val) if @required || (hash_like?(val) && val.key?(attr_name)) rescue Grape::Exceptions::Validation => e array_errors << e end @@ -57,23 +69,33 @@ def validate!(params) raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end - def self.inherited(klass) - super - Validations.register(klass) + def hash_like?(obj) + obj.respond_to?(:key?) + end + + def options_key?(key) + hash_like?(@option) && @option.key?(key) && !@option[key].nil? end def message(default_key = nil) - options = instance_variable_get(:@option) - options_key?(:message) ? options[:message] : default_key + key = options_key?(:message) ? @option[:message] : default_key + return key if key + + yield if block_given? 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? + def option_value + options_key?(:value) ? @option[:value] : @option end - def fail_fast? - @fail_fast + def translate_message(key, **) + translate(key, scope: 'grape.errors.messages', **) + end + + def scrub(value) + return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding? + + value.scrub end end end diff --git a/lib/grape/validations/validators/coerce_validator.rb b/lib/grape/validations/validators/coerce_validator.rb index eaf7c4069..5f0477886 100644 --- a/lib/grape/validations/validators/coerce_validator.rb +++ b/lib/grape/validations/validators/coerce_validator.rb @@ -7,19 +7,22 @@ class CoerceValidator < Base def initialize(attrs, options, required, scope, opts) super - @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) - type - else - Types.build_coercer(type, method: @option[:method]) - end + type = hash_like?(@option[:type]) ? @option[:type][:value] : @option[:type] + @converter = + if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) + type + else + Types.build_coercer(type, method: @option[:method]) + end + @exception_message = message(:coerce) end def validate_param!(attr_name, params) - raise validation_exception(attr_name) unless params.is_a? Hash + raise validation_exception(attr_name) unless hash_like?(params) new_value = coerce_value(params[attr_name]) - raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) + raise validation_exception(attr_name, new_value.message) if new_value.is_a?(Types::InvalidValue) # 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 @@ -37,36 +40,17 @@ def validate_param!(attr_name, params) private - # @!attribute [r] converter - # Object that will be used for parameter coercion and type checking. - # - # See {Types.build_coercer} - # - # @return [Object] - attr_reader :converter - - def valid_type?(val) - !val.is_a?(Types::InvalidValue) - end - def coerce_value(val) - converter.call(val) + @converter.call(val) # Some custom types might fail, so it should be treated as an invalid value rescue StandardError Types::InvalidValue.new end - # Type to which the parameter will be coerced. - # - # @return [Class] - def type - @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] - end - def validation_exception(attr_name, custom_msg = nil) Grape::Exceptions::Validation.new( - params: [@scope.full_name(attr_name)], - message: custom_msg || message(:coerce) + params: @scope.full_name(attr_name), + message: custom_msg || @exception_message ) end end diff --git a/lib/grape/validations/validators/contract_scope_validator.rb b/lib/grape/validations/validators/contract_scope_validator.rb index b8a3365c1..39571d228 100644 --- a/lib/grape/validations/validators/contract_scope_validator.rb +++ b/lib/grape/validations/validators/contract_scope_validator.rb @@ -3,12 +3,9 @@ module Grape module Validations module Validators - class ContractScopeValidator < Base - attr_reader :schema - - def initialize(_attrs, _options, _required, _scope, opts) - super - @schema = opts.fetch(:schema) + class ContractScopeValidator + def initialize(schema:) + @schema = schema end # Validates a given request. @@ -16,7 +13,7 @@ def initialize(_attrs, _options, _required, _scope, opts) # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed # @return [void] def validate(request) - res = schema.call(request.params) + res = @schema.call(request.params) if res.success? request.params.deep_merge!(res.to_h) @@ -26,13 +23,17 @@ def validate(request) raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) end + def fail_fast? + false + end + private def build_errors_from_messages(messages) messages.map do |message| full_name = message.path.first.to_s full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 - Grape::Exceptions::Validation.new(params: [full_name], message: message.text) + Grape::Exceptions::Validation.new(params: full_name, message: message.text) end end end diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default_validator.rb index eba8c7730..af40f20d1 100644 --- a/lib/grape/validations/validators/default_validator.rb +++ b/lib/grape/validations/validators/default_validator.rb @@ -4,23 +4,17 @@ module Grape module Validations module Validators class DefaultValidator < Base - def initialize(attrs, options, required, scope, opts = {}) - @default = options + def initialize(attrs, options, required, scope, opts) super - end - - def validate_param!(attr_name, params) - params[attr_name] = if @default.is_a? Proc - if @default.parameters.empty? - @default.call - else - @default.call(params) - end - elsif @default.frozen? || !@default.duplicable? - @default - else - @default.dup - end + # !important, lazy call at runtime + @default_call = + if @option.is_a?(Proc) + @option.arity.zero? ? ->(_p) { @option.call } : ->(p) { @option.call(p) } + elsif @option.frozen? || !@option.duplicable? + ->(_p) { @option } + else + ->(_p) { @option.dup } + end end def validate!(params) @@ -28,7 +22,7 @@ def validate!(params) attrs.each do |resource_params, attr_name| next unless @scope.meets_dependency?(resource_params, params) - validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? + resource_params[attr_name] = @default_call.call(resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end end diff --git a/lib/grape/validations/validators/exactly_one_of_validator.rb b/lib/grape/validations/validators/exactly_one_of_validator.rb index aa1c54711..22fe531a3 100644 --- a/lib/grape/validations/validators/exactly_one_of_validator.rb +++ b/lib/grape/validations/validators/exactly_one_of_validator.rb @@ -4,12 +4,18 @@ module Grape module Validations module Validators class ExactlyOneOfValidator < MultipleParamsBase + def initialize(attrs, options, required, scope, opts) + super + @exactly_one_exception_message = message(:exactly_one) + @mutual_exclusion_exception_message = message(:mutual_exclusion) + end + def validate_params!(params) keys = keys_in_common(params) return if keys.length == 1 - raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.empty? + raise Grape::Exceptions::Validation.new(params: all_keys, message: @exactly_one_exception_message) if keys.empty? - raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + raise Grape::Exceptions::Validation.new(params: keys, message: @mutual_exclusion_exception_message) end end end diff --git a/lib/grape/validations/validators/except_values_validator.rb b/lib/grape/validations/validators/except_values_validator.rb index 298eb0ab9..040fa9408 100644 --- a/lib/grape/validations/validators/except_values_validator.rb +++ b/lib/grape/validations/validators/except_values_validator.rb @@ -5,18 +5,31 @@ module Validations module Validators class ExceptValuesValidator < Base def initialize(attrs, options, required, scope, opts) - @except = options.is_a?(Hash) ? options[:value] : options super + except = option_value + raise ArgumentError, 'except_values Proc must have arity of zero' if except.is_a?(Proc) && !except.arity.zero? + + # important! lazy call at runtime + @excepts_call = + if except.is_a?(Proc) + -> { except.call } + else + -> { except } + end + @exception_message = message(:except_values) end def validate_param!(attr_name, params) - return unless params.respond_to?(:key?) && params.key?(attr_name) + return unless hash_like?(params) && params.key?(attr_name) - excepts = @except.is_a?(Proc) ? @except.call : @except + excepts = @excepts_call.call return if excepts.nil? param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } + raise Grape::Exceptions::Validation.new( + params: @scope.full_name(attr_name), + message: @exception_message + ) if param_array.any? { |param| excepts.include?(param) } end end end diff --git a/lib/grape/validations/validators/length_validator.rb b/lib/grape/validations/validators/length_validator.rb index c84b4c096..1154ed906 100644 --- a/lib/grape/validations/validators/length_validator.rb +++ b/lib/grape/validations/validators/length_validator.rb @@ -5,19 +5,19 @@ module Validations module Validators class LengthValidator < Base def initialize(attrs, options, required, scope, opts) - @min = options[:min] - @max = options[:max] - @is = options[:is] - super - raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) - raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) - raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max + @min, @max, @is = @option.values_at(:min, :max, :is) + raise ArgumentError, 'min must be an integer greater than or equal to zero' if @min && (!@min.is_a?(Integer) || @min.negative?) + raise ArgumentError, 'max must be an integer greater than or equal to zero' if @max && (!@max.is_a?(Integer) || @max.negative?) + raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if @min && @max && @min > @max + + if @is + raise ArgumentError, 'is must be an integer greater than zero' unless @is.is_a?(Integer) && @is.positive? + raise ArgumentError, 'is cannot be combined with min or max' if @min || @max + end - return if @is.nil? - raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive? - raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil? + @exception_message = message { build_exception_message } end def validate_param!(attr_name, params) @@ -25,22 +25,22 @@ def validate_param!(attr_name, params) return unless param.respond_to?(:length) - return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is) + return unless (@min && param.length < @min) || (@max && param.length > @max) || (@is && param.length != @is) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end - def build_message - if options_key?(:message) - @option[:message] - elsif @min && @max - format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max + private + + def build_exception_message + if @min && @max + { key: :length, min: @min, max: @max }.freeze elsif @min - format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min + { key: :length_min, min: @min }.freeze elsif @max - format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max + { key: :length_max, max: @max }.freeze else - format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is + { key: :length_is, is: @is }.freeze end end end diff --git a/lib/grape/validations/validators/mutually_exclusive_validator.rb b/lib/grape/validations/validators/mutually_exclusive_validator.rb index 8e98bbfc6..278cfa550 100644 --- a/lib/grape/validations/validators/mutually_exclusive_validator.rb +++ b/lib/grape/validations/validators/mutually_exclusive_validator.rb @@ -4,11 +4,16 @@ module Grape module Validations module Validators class MutuallyExclusiveValidator < MultipleParamsBase + def initialize(attrs, options, required, scope, opts) + super + @exception_message = message(:mutual_exclusion) + end + def validate_params!(params) keys = keys_in_common(params) return if keys.length <= 1 - raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) + raise Grape::Exceptions::Validation.new(params: keys, message: @exception_message) end end end diff --git a/lib/grape/validations/validators/presence_validator.rb b/lib/grape/validations/validators/presence_validator.rb index ae31dc3fb..9236fd0fd 100644 --- a/lib/grape/validations/validators/presence_validator.rb +++ b/lib/grape/validations/validators/presence_validator.rb @@ -4,10 +4,15 @@ module Grape module Validations module Validators class PresenceValidator < Base + def initialize(attrs, options, required, scope, opts) + super + @exception_message = message(:presence) + end + def validate_param!(attr_name, params) - return if params.respond_to?(:key?) && params.key?(attr_name) + return if hash_like?(params) && params.key?(attr_name) - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end end end diff --git a/lib/grape/validations/validators/regexp_validator.rb b/lib/grape/validations/validators/regexp_validator.rb index 7d30baf97..4a37dfc33 100644 --- a/lib/grape/validations/validators/regexp_validator.rb +++ b/lib/grape/validations/validators/regexp_validator.rb @@ -4,21 +4,18 @@ module Grape module Validations module Validators class RegexpValidator < Base - def validate_param!(attr_name, params) - return unless params.respond_to?(:key) && params.key?(attr_name) - - value = options_key?(:value) ? @option[:value] : @option - return if Array.wrap(params[attr_name]).all? { |param| param.nil? || scrub(param.to_s).match?(value) } - - raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) + def initialize(attrs, options, required, scope, opts) + super + @value = option_value + @exception_message = message(:regexp) end - private + def validate_param!(attr_name, params) + return unless hash_like?(params) && params.key?(attr_name) - def scrub(param) - return param if param.valid_encoding? + return if Array.wrap(params[attr_name]).all? { |param| param.nil? || scrub(param.to_s).match?(@value) } - param.scrub + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end end end diff --git a/lib/grape/validations/validators/same_as_validator.rb b/lib/grape/validations/validators/same_as_validator.rb index 5a65afa60..70ebd6d65 100644 --- a/lib/grape/validations/validators/same_as_validator.rb +++ b/lib/grape/validations/validators/same_as_validator.rb @@ -4,24 +4,16 @@ module Grape module Validations module Validators class SameAsValidator < Base - def validate_param!(attr_name, params) - confirmation = options_key?(:value) ? @option[:value] : @option - return if params[attr_name] == params[confirmation] - - raise Grape::Exceptions::Validation.new( - params: [@scope.full_name(attr_name)], - message: build_message - ) + def initialize(attrs, options, required, scope, opts) + super + @value = option_value + @exception_message = message { { key: :same_as, parameter: @option }.freeze } end - private + def validate_param!(attr_name, params) + return if params[attr_name] == params[@value] - def build_message - if options_key?(:message) - @option[:message] - else - format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option - end + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end end end diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 85089e7e2..69bfd1271 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -5,34 +5,36 @@ module Validations module Validators class ValuesValidator < Base def initialize(attrs, options, required, scope, opts) - @values = options.is_a?(Hash) ? options[:value] : options super + values = option_value + raise ArgumentError, 'values Proc must have arity of zero or one' if values.is_a?(Proc) && values.arity > 1 + + # important! lazy call at runtime + @values_call = + if values.is_a?(Proc) && values.arity.zero? + -> { values.call } + else + -> { values } + end + @exception_message = message(:values) end def validate_param!(attr_name, params) - return unless params.is_a?(Hash) + return unless hash_like?(params) - val = params[attr_name] + val = scrub(params[attr_name]) return if val.nil? && !required_for_root_scope? - - val = val.scrub if val.respond_to?(:valid_encoding?) && !val.valid_encoding? - - # don't forget that +false.blank?+ is true return if val != false && val.blank? && @allow_blank - return if check_values?(val, attr_name) - raise Grape::Exceptions::Validation.new( - params: [@scope.full_name(attr_name)], - message: message(:values) - ) + raise Grape::Exceptions::Validation.new(params: @scope.full_name(attr_name), message: @exception_message) end private def check_values?(val, attr_name) - values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values + values = @values_call.call return true if values.nil? param_array = val.nil? ? [nil] : Array.wrap(val) diff --git a/spec/config/spec_test_prof.rb b/spec/config/spec_test_prof.rb deleted file mode 100644 index e5259e110..000000000 --- a/spec/config/spec_test_prof.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'test_prof/recipes/rspec/let_it_be' - -TestProf::BeforeAll.adapter = Class.new do - def begin_transaction; end - - def rollback_transaction; end -end.new diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 27c9f2736..1d0808271 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -1062,9 +1062,6 @@ def memoized have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), - have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), - validators: [], - request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), @@ -1091,9 +1088,6 @@ def memoized have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), - have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), - validators: [], - request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), diff --git a/spec/grape/validations/validators/all_or_none_validator_spec.rb b/spec/grape/validations/validators/all_or_none_validator_spec.rb index 9c8fe78b1..df491f706 100644 --- a/spec/grape/validations/validators/all_or_none_validator_spec.rb +++ b/spec/grape/validations/validators/all_or_none_validator_spec.rb @@ -1,101 +1,100 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AllOrNoneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, :other, type: Grape::API::Boolean - all_or_none_of :beer, :wine - end - post 'mixed-params' do + params do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + post do + end + end end - params do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine, message: 'choose all or none' - end - post '/custom-message' do - end + context 'when all restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true } } - params do - requires :item, type: Hash do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-hash' do - end - params do - requires :items, type: Array do - optional :beer, :wine, type: Grape::API::Boolean - all_or_none_of :beer, :wine + context 'when a subset of restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine' => ['provide all or none of parameters'] + ) end end - post '/nested-array' do + + context 'when no restricted params are present' do + let(:path) { '/' } + let(:params) { { somethingelse: true } } + + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end end + end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, type: Grape::API::Boolean + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :other, type: Grape::API::Boolean all_or_none_of :beer, :wine end + post 'mixed-params' do + end end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end - - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, other: true } } - - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 - end - end end - context 'when a subset of restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine' => ['provide all or none of parameters'] - ) + params do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine, message: 'choose all or none' + end + post '/custom-message' do + end + end end - end - context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true } } @@ -108,17 +107,24 @@ end end - context 'when no restricted params are present' do - let(:path) { '/' } - let(:params) { { somethingelse: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-hash' do + end + end end - end - context 'when restricted params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true } } } @@ -131,7 +137,24 @@ end end - context 'when mutually exclusive params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true }] } } @@ -144,7 +167,26 @@ end end - context 'when mutually exclusive params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, type: Grape::API::Boolean + all_or_none_of :beer, :wine + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true }] }] } } diff --git a/spec/grape/validations/validators/allow_blank_validator_spec.rb b/spec/grape/validations/validators/allow_blank_validator_spec.rb index 88c527508..2fb3f7df4 100644 --- a/spec/grape/validations/validators/allow_blank_validator_spec.rb +++ b/spec/grape/validations/validators/allow_blank_validator_spec.rb @@ -1,145 +1,109 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AllowBlankValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json - - params do - requires :name, allow_blank: false - end - get '/disallow_blank' - - params do - optional :name, type: String, allow_blank: false - end - get '/opt_disallow_string_blank' - - params do - optional :name, allow_blank: false - 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: false - 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' + describe 'bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :val, type: Integer, allow_blank: true + params do + requires :name, type: String, allow_blank: false + end + get '/bad_encoding' end - get '/allow_integer_blank' + end - params do - requires :val, type: Float, allow_blank: true + context 'when value has bad encoding' do + it 'does not raise an error' do + expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error end - get '/allow_float_blank' + end + end - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' + describe '/disallow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :val, type: Symbol, allow_blank: true + params do + requires :name, allow_blank: false + end + get '/disallow_blank' end - get '/allow_symbol_blank' + end - params do - requires :val, type: Grape::API::Boolean, allow_blank: true - end - get '/allow_boolean_blank' + it 'refuses empty string' do + get '/disallow_blank', name: '' + expect(last_response.status).to eq(400) + end - params do - requires :val, type: Grape::API::Boolean, allow_blank: false - end - get '/disallow_boolean_blank' + it 'refuses only whitespaces' do + get '/disallow_blank', name: ' ' + expect(last_response.status).to eq(400) - params do - optional :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_blank_required_param_in_an_optional_group' + get '/disallow_blank', name: " \n " + expect(last_response.status).to eq(400) - 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' + get '/disallow_blank', name: "\n" + expect(last_response.status).to eq(400) + end - params do - optional :user, type: Hash do - optional :name, allow_blank: false - requires :age - end - end - get '/disallow_blank_optional_param_in_an_optional_group' + it 'refuses nil' do + get '/disallow_blank', name: nil + expect(last_response.status).to eq(400) + end - params do - requires :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_blank_required_param_in_a_required_group' + it 'refuses missing' do + get '/disallow_blank' + expect(last_response.status).to eq(400) + end - params do - requires :user, type: Hash do - requires :name, allow_blank: false - end - end - get '/disallow_string_value_in_a_required_hash_group' + it 'accepts valid input' do + get '/disallow_blank', name: 'bob' + expect(last_response.status).to eq(200) + end + end - params do - requires :user, type: Hash do - optional :name, allow_blank: false - end - end - get '/disallow_blank_optional_param_in_a_required_group' + describe '/opt_disallow_string_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :user, type: Hash do - optional :name, allow_blank: false + params do + optional :name, type: String, allow_blank: false end + get '/opt_disallow_string_blank' end - get '/disallow_string_value_in_an_optional_hash_group' + end - resources :custom_message do - params do - requires :name, allow_blank: { value: false, message: 'has no value' } - end - get + it 'allows missing optional strings' do + get 'opt_disallow_string_blank' + expect(last_response.status).to eq(200) + end + end - params do - optional :name, allow_blank: { value: false, message: 'has no value' } - end - get '/disallow_blank_optional_param' + describe '/allow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :name, allow_blank: true end get '/allow_blank' + end + end + + it 'accepts empty input when allow_blank is true' do + get '/allow_blank', name: '' + expect(last_response.status).to eq(200) + end + end + + describe 'type-specific blanks' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :val, type: DateTime, allow_blank: true @@ -147,7 +111,7 @@ get '/allow_datetime_blank' params do - requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } + requires :val, type: DateTime, allow_blank: false end get '/disallow_datetime_blank' @@ -171,11 +135,6 @@ end get '/allow_float_blank' - params do - requires :val, type: Integer, allow_blank: true - end - get '/allow_integer_blank' - params do requires :val, type: Symbol, allow_blank: true end @@ -187,13 +146,73 @@ get '/allow_boolean_blank' params do - requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' } + requires :val, type: Grape::API::Boolean, allow_blank: false end get '/disallow_boolean_blank' + end + end + + it 'refuses empty string for disallow_datetime_blank' do + get '/disallow_datetime_blank', val: '' + expect(last_response.status).to eq(400) + end + + it 'accepts value when time allow_blank' do + get '/disallow_datetime_blank', val: Time.now + expect(last_response.status).to eq(200) + end + + it 'accepts empty when datetime allow_blank' do + get '/allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty input' do + get '/default_allow_datetime_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when date allow_blank' do + get '/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 '/allow_integer_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when float allow_blank' do + get '/allow_float_blank', val: '' + expect(last_response.status).to eq(200) + end + end + + it 'accepts empty when symbol allow_blank' do + get '/allow_symbol_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts empty when boolean allow_blank' do + get '/allow_boolean_blank', val: '' + expect(last_response.status).to eq(200) + end + + it 'accepts false when boolean allow_blank' do + get '/disallow_boolean_blank', val: false + expect(last_response.status).to eq(200) + end + end + + describe 'in an optional group' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do optional :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } + requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_an_optional_group' @@ -207,94 +226,238 @@ params do optional :user, type: Hash do - optional :name, allow_blank: { value: false, message: 'has no value' } + optional :name, allow_blank: false requires :age end end get '/disallow_blank_optional_param_in_an_optional_group' + end + end + + context 'as a required param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/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 '/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 '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } + expect(last_response.status).to eq(400) + end + end + + context 'as an optional param' do + it 'accepts a missing group, even with a disallwed blank param' do + get '/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 '/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 '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + end + end + end + + describe 'in a required group' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do requires :user, type: Hash do - requires :name, allow_blank: { value: false, message: 'has no value' } + requires :name, allow_blank: false 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' } + requires :name, allow_blank: false 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' } + optional :name, allow_blank: false 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' } + optional :name, allow_blank: false end end get '/disallow_string_value_in_an_optional_hash_group' end end - end - describe 'bad encoding' do - let(:app) do - Class.new(Grape::API) do - default_format :json + context 'as a required param' do + it 'refuses a blank value in a required existing group' do + get '/disallow_blank_required_param_in_a_required_group', user: { name: '' } + expect(last_response.status).to eq(400) + end - params do - requires :name, type: String, allow_blank: false - end - get '/bad_encoding' + it 'refuses a string value in a required hash group' do + get '/disallow_string_value_in_a_required_hash_group', user: '' + expect(last_response.status).to eq(400) end end - context 'when value has bad encoding' do - it 'does not raise an error' do - expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error + context 'as an optional param' do + it 'accepts a nested missing value' do + get '/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 '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } + expect(last_response.status).to eq(400) + end + + it 'refuses a string value in an optional hash group' do + get '/disallow_string_value_in_an_optional_hash_group', user: '' + expect(last_response.status).to eq(400) end end end - context 'invalid input' do - it 'refuses empty string' do - get '/disallow_blank', name: '' - expect(last_response.status).to eq(400) + describe 'custom message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - get '/disallow_datetime_blank', val: '' - expect(last_response.status).to eq(400) - end + resources :custom_message do + params do + requires :name, allow_blank: { value: false, message: 'has no value' } + end + get - it 'refuses only whitespaces' do - get '/disallow_blank', name: ' ' - expect(last_response.status).to eq(400) + params do + optional :name, allow_blank: { value: false, message: 'has no value' } + end + get '/disallow_blank_optional_param' - get '/disallow_blank', name: " \n " - expect(last_response.status).to eq(400) + params do + requires :name, allow_blank: true + end + get '/allow_blank' - get '/disallow_blank', name: "\n" - expect(last_response.status).to eq(400) - end + params do + requires :val, type: DateTime, allow_blank: true + end + get '/allow_datetime_blank' - it 'refuses nil' do - get '/disallow_blank', name: nil - expect(last_response.status).to eq(400) - end + params do + requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' } + end + get '/disallow_datetime_blank' - it 'refuses missing' do - get '/disallow_blank' - expect(last_response.status).to eq(400) + 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: Integer, allow_blank: true + end + get '/allow_integer_blank' + + params do + requires :val, type: Symbol, allow_blank: true + end + get '/allow_symbol_blank' + + params do + requires :val, type: Grape::API::Boolean, allow_blank: true + end + get '/allow_boolean_blank' + + params do + requires :val, type: Grape::API::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 - context 'custom validation message' do context 'with invalid input' do it 'refuses empty string' do get '/custom_message', name: '' @@ -455,140 +618,4 @@ end end end - - context 'valid input' do - it 'allows missing optional strings' do - get 'opt_disallow_string_blank' - expect(last_response.status).to eq(200) - end - - it 'accepts valid input' do - get '/disallow_blank', name: 'bob' - expect(last_response.status).to eq(200) - end - - it 'accepts empty input when allow_blank is false' do - get '/allow_blank', name: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty input' do - get '/default_allow_datetime_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when datetime allow_blank' do - get '/allow_datetime_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when date allow_blank' do - get '/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 '/allow_integer_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when float allow_blank' do - get '/allow_float_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when integer allow_blank' do - get '/allow_integer_blank', val: '' - expect(last_response.status).to eq(200) - end - end - - it 'accepts empty when symbol allow_blank' do - get '/allow_symbol_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts empty when boolean allow_blank' do - get '/allow_boolean_blank', val: '' - expect(last_response.status).to eq(200) - end - - it 'accepts false when boolean allow_blank' do - get '/disallow_boolean_blank', val: false - expect(last_response.status).to eq(200) - end - - it 'accepts value when time allow_blank' do - get '/disallow_datetime_blank', val: Time.now - 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 '/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 '/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 '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } - expect(last_response.status).to eq(400) - end - end - - context 'as an optional param' do - it 'accepts a missing group, even with a disallwed blank param' do - get '/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 '/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 '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } - expect(last_response.status).to eq(400) - 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 '/disallow_blank_required_param_in_a_required_group', user: { name: '' } - expect(last_response.status).to eq(400) - end - - it 'refuses a string value in a required hash group' do - get '/disallow_string_value_in_a_required_hash_group', user: '' - expect(last_response.status).to eq(400) - end - end - - context 'as an optional param' do - it 'accepts a nested missing value' do - get '/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 '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } - expect(last_response.status).to eq(400) - end - - it 'refuses a string value in an optional hash group' do - get '/disallow_string_value_in_an_optional_hash_group', user: '' - expect(last_response.status).to eq(400) - end - end - end end diff --git a/spec/grape/validations/validators/at_least_one_of_validator_spec.rb b/spec/grape/validations/validators/at_least_one_of_validator_spec.rb index 4b0e3b0c7..a75ecae25 100644 --- a/spec/grape/validations/validators/at_least_one_of_validator_spec.rb +++ b/spec/grape/validations/validators/at_least_one_of_validator_spec.rb @@ -1,79 +1,61 @@ # frozen_string_literal: true describe Grape::Validations::Validators::AtLeastOneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer, :wine, :grapefruit, :other - at_least_one_of :beer, :wine, :grapefruit - end - post 'mixed-params' do + params do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit + end + post do + end + end end - params do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' - end - post '/custom-message' do - end + context 'when all restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :item, type: Hash do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-hash' do - end - params do - requires :items, type: Array do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' - end - end - post '/nested-array' do - end + context 'when a subset of restricted params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit - at_least_one_of :beer, :wine, :grapefruit, message: 'fail' - end + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + context 'when none of the restricted params is selected' do + let(:path) { '/' } + let(:params) { { other: true } } - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] + ) + end end - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } + context 'when exactly one of the restricted params is selected' do + let(:path) { '/' } + let(:params) { { beer: true } } it 'does not return a validation error' do validate @@ -82,9 +64,24 @@ end end - context 'when a subset of restricted params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :grapefruit, :other + at_least_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + end + end + end + + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'does not return a validation error' do validate @@ -92,42 +89,52 @@ end end - context 'when none of the restricted params is selected' do - let(:path) { '/' } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' + end + post '/custom-message' do + end + end + end + + let(:path) { '/custom-message' } let(:params) { { other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] + 'beer,wine,grapefruit' => ['you should choose something'] ) end - - context 'when custom message is specified' do - let(:path) { '/custom-message' } - - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['you should choose something'] - ) - end - end end - context 'when exactly one of the restricted params is selected' do - let(:path) { '/' } - let(:params) { { beer: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + post '/nested-hash' do + end + end end - end - context 'when restricted params are nested inside hash' do let(:path) { '/nested-hash' } context 'when at least one of them is present' do @@ -152,7 +159,24 @@ end end - context 'when restricted params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } context 'when at least one of them is present' do @@ -177,7 +201,26 @@ end end - context 'when restricted params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit + at_least_one_of :beer, :wine, :grapefruit, message: 'fail' + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } context 'when at least one of them is present' do diff --git a/spec/grape/validations/validators/base_i18n_spec.rb b/spec/grape/validations/validators/base_i18n_spec.rb new file mode 100644 index 000000000..3bc61b154 --- /dev/null +++ b/spec/grape/validations/validators/base_i18n_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +describe Grape::Validations::Validators::Base do + describe 'i18n' do + subject { Class.new(Grape::API) } + + let(:app) { subject } + + let(:custom_i18n_validator) do + Class.new(Grape::Validations::Validators::Base) do + def initialize(attrs, options, required, scope, opts) + super + @exception_message = message(:custom_i18n_test) + end + + def validate_param!(attr_name, params) + return if hash_like?(params) && params[attr_name] == 'accept' + + raise Grape::Exceptions::Validation.new( + params: @scope.full_name(attr_name), + message: @exception_message + ) + end + end + end + + before do + I18n.available_locales = %i[en zh-CN] + I18n.backend.store_translations(:en, grape: { errors: { messages: { custom_i18n_test: 'custom validation failed (en)' } } }) + I18n.backend.store_translations(:'zh-CN', grape: { errors: { messages: { custom_i18n_test: '自定义校验失败 (zh-CN)' } } }) + stub_const('CustomI18nValidator', custom_i18n_validator) + Grape::Validations.register(CustomI18nValidator) + end + + after do + Grape::Validations.deregister('custom_i18n') + I18n.available_locales = [] + end + + it 'uses the request-time locale regardless of the locale active at definition time' do + # Define the API while zh-CN is the active locale + I18n.with_locale(:'zh-CN') do + subject.params do + requires :token, custom_i18n: true + end + subject.post do + end + end + # Switch to English before making the request + I18n.with_locale(:en) do + post '/', token: 'reject' + end + + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('token custom validation failed (en)') + end + + it 'uses zh-CN message when request is made with zh-CN locale' do + I18n.with_locale(:en) do + subject.params do + requires :token, custom_i18n: true + end + subject.post do + end + end + + I18n.with_locale(:'zh-CN') do + post '/', token: 'reject' + end + + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('token 自定义校验失败 (zh-CN)') + end + end +end diff --git a/spec/grape/validations/validators/contract_scope_validator_spec.rb b/spec/grape/validations/validators/contract_scope_validator_spec.rb deleted file mode 100644 index b8d462c31..000000000 --- a/spec/grape/validations/validators/contract_scope_validator_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -describe Grape::Validations::Validators::ContractScopeValidator do - describe '.inherits' do - subject { described_class } - - it { is_expected.to be < Grape::Validations::Validators::Base } - end -end diff --git a/spec/grape/validations/validators/default_validator_spec.rb b/spec/grape/validations/validators/default_validator_spec.rb index df84f06ee..92e9066b7 100644 --- a/spec/grape/validations/validators/default_validator_spec.rb +++ b/spec/grape/validations/validators/default_validator_spec.rb @@ -1,191 +1,270 @@ # frozen_string_literal: true describe Grape::Validations::Validators::DefaultValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :id - optional :type, default: 'default-type' - end - get '/' do - { id: params[:id], type: params[:type] } + params do + optional :id + optional :type, default: 'default-type' + end + get '/' do + { id: params[:id], type: params[:type] } + end end + end - params do - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end - get '/user' do - { type1: params[:type1], type2: params[:type2] } - end + it 'set default value for optional param' do + get('/') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) + end + end - params do - requires :id - optional :type1, default: 'default-type1' - optional :type2, default: 'default-type2' - end + describe '/user' do + let(:app) do + Class.new(Grape::API) do + default_format :json - get '/message' do - { id: params[:id], type1: params[:type1], type2: params[:type2] } + params do + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' + end + get '/user' do + { type1: params[:type1], type2: params[:type2] } + end end + end - params do - optional :random, default: -> { Random.rand } - optional :not_random, default: Random.rand - end - get '/numbers' do - { random_number: params[:random], non_random_number: params[:non_random_number] } - end + it 'set default values for optional params' do + get('/user') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) + end + + it 'set default values for missing params in the request' do + get('/user?type2=value2') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) + end + end + + describe '/message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :array, type: Array do - requires :name - optional :with_default, default: 'default' + params do + requires :id + optional :type1, default: 'default-type1' + optional :type2, default: 'default-type2' end - end - get '/array' do - { array: params[:array] } - end - params do - requires :thing1 - optional :more_things, type: Array do - requires :nested_thing - requires :other_thing, default: 1 + get '/message' do + { id: params[:id], type1: params[:type1], type2: params[:type2] } end end - get '/optional_array' do - { thing1: params[:thing1] } + end + + it 'set default values for optional params and allow to use required fields in the same time' do + get('/message?id=1') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) + end + end + + describe '/numbers' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :random, default: -> { Random.rand } + optional :not_random, default: Random.rand + end + get '/numbers' do + { random_number: params[:random], non_random_number: params[:non_random_number] } + end end + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - requires :name, type: String - requires :value, type: String - end + it 'sets lambda based defaults at the time of call' do + get('/numbers') + expect(last_response.status).to eq(200) + before = JSON.parse(last_response.body) + get('/numbers') + expect(last_response.status).to eq(200) + after = JSON.parse(last_response.body) + + expect(before['non_random_number']).to eq(after['non_random_number']) + expect(before['random_number']).not_to eq(after['random_number']) + end + end + + describe '/array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :array, type: Array do + requires :name + optional :with_default, default: 'default' end end + get '/array' do + { array: params[:array] } + end end - get '/nested_optional_array' do - { root: params[:root] } - end + end - params do - requires :root, type: Hash do - optional :some_things, type: Array do - requires :foo - optional :options, type: Array do - optional :name, type: String - optional :value, type: String - end + it 'sets default values for grouped arrays' do + get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) + end + end + + describe '/optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :thing1 + optional :more_things, type: Array do + requires :nested_thing + requires :other_thing, default: 1 end end + get '/optional_array' do + { thing1: params[:thing1] } + end end - get '/another_nested_optional_array' do - { root: params[:root] } - end + end - params do - requires :foo - optional :bar, default: ->(params) { params[:foo] } - optional :qux, default: ->(params) { params[:bar] } - end - get '/default_values_from_other_params' do - { - foo: params[:foo], - bar: params[:bar], - qux: params[:qux] - } - end + it 'lets you leave required values nested inside an optional blank' do + get '/optional_array', thing1: 'stuff' + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) end end - it 'lets you leave required values nested inside an optional blank' do - get '/optional_array', thing1: 'stuff' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) - end + describe '/nested_optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows optional arrays to be omitted' do - params = { some_things: - [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, - { foo: 'two' }, - { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } - get '/nested_optional_array', root: params - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ root: params }.to_json) - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + requires :name, type: String + requires :value, type: String + end + end + end + end + get '/nested_optional_array' do + { root: params[:root] } + end + end + end - it 'does not allows faulty optional arrays' do - params = { some_things: - [ - { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, - { foo: 'two', options: [{ name: 'wat' }] }, - { foo: 'three' } - ] } - error = { error: 'root[some_things][1][options][0][value] is missing' } - get '/nested_optional_array', root: params - expect(last_response.status).to eq(400) - expect(last_response.body).to eq(error.to_json) - end + it 'allows optional arrays to be omitted' do + params = { some_things: + [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, + { foo: 'two' }, + { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } + get '/nested_optional_array', root: params + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ root: params }.to_json) + end - it 'allows optional arrays with optional params' do - params = { some_things: - [ - { foo: 'one', options: [{ value: 'nope' }] }, - { foo: 'two', options: [{ name: 'wat' }] }, - { foo: 'three' } - ] } - get '/another_nested_optional_array', root: params - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ root: params }.to_json) + it 'does not allows faulty optional arrays' do + params = { some_things: + [ + { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, + { foo: 'two', options: [{ name: 'wat' }] }, + { foo: 'three' } + ] } + error = { error: 'root[some_things][1][options][0][value] is missing' } + get '/nested_optional_array', root: params + expect(last_response.status).to eq(400) + expect(last_response.body).to eq(error.to_json) + end end - it 'set default value for optional param' do - get('/') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) - end + describe '/another_nested_optional_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'set default values for optional params' do - get('/user') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) - end + params do + requires :root, type: Hash do + optional :some_things, type: Array do + requires :foo + optional :options, type: Array do + optional :name, type: String + optional :value, type: String + end + end + end + end + get '/another_nested_optional_array' do + { root: params[:root] } + end + end + end - it 'set default values for missing params in the request' do - get('/user?type2=value2') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) + it 'allows optional arrays with optional params' do + params = { some_things: + [ + { foo: 'one', options: [{ value: 'nope' }] }, + { foo: 'two', options: [{ name: 'wat' }] }, + { foo: 'three' } + ] } + get '/another_nested_optional_array', root: params + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ root: params }.to_json) + end end - it 'set default values for optional params and allow to use required fields in the same time' do - get('/message?id=1') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) - end + describe '/default_values_from_other_params' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'sets lambda based defaults at the time of call' do - get('/numbers') - expect(last_response.status).to eq(200) - before = JSON.parse(last_response.body) - get('/numbers') - expect(last_response.status).to eq(200) - after = JSON.parse(last_response.body) + params do + requires :foo + optional :bar, default: ->(params) { params[:foo] } + optional :qux, default: ->(params) { params[:bar] } + end + get '/default_values_from_other_params' do + { + foo: params[:foo], + bar: params[:bar], + qux: params[:qux] + } + end + end + end - expect(before['non_random_number']).to eq(after['non_random_number']) - expect(before['random_number']).not_to eq(after['random_number']) - end + it 'sets default value for optional params using other params values' do + expected_foo_value = 'foo-value' - it 'sets default values for grouped arrays' do - get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) + get("/default_values_from_other_params?foo=#{expected_foo_value}") + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq({ + foo: expected_foo_value, + bar: expected_foo_value, + qux: expected_foo_value + }.to_json) + end end context 'optional group with defaults' do @@ -473,17 +552,4 @@ def app expect(JSON.parse(last_response.body)).to eq(expected) end end - - it 'sets default value for optional params using other params values' do - expected_foo_value = 'foo-value' - - get("/default_values_from_other_params?foo=#{expected_foo_value}") - - expect(last_response.status).to eq(200) - expect(last_response.body).to eq({ - foo: expected_foo_value, - bar: expected_foo_value, - qux: expected_foo_value - }.to_json) - end end diff --git a/spec/grape/validations/validators/exactly_one_of_validator_spec.rb b/spec/grape/validations/validators/exactly_one_of_validator_spec.rb index 21e177e93..90c058690 100644 --- a/spec/grape/validations/validators/exactly_one_of_validator_spec.rb +++ b/spec/grape/validations/validators/exactly_one_of_validator_spec.rb @@ -1,165 +1,158 @@ # frozen_string_literal: true describe Grape::Validations::Validators::ExactlyOneOfValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end - - params do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit - end - post do - end - - params do - optional :beer - optional :wine - optional :grapefruit - optional :other - exactly_one_of :beer, :wine, :grapefruit - end - post 'mixed-params' do - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' - end - post '/custom-message' do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - requires :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit + params do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + post do + end end end - post '/nested-hash' do - end - params do - optional :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit - end - end - post '/nested-optional-hash' do - end + context 'when all params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :items, type: Array do - optional :beer - optional :wine - optional :grapefruit - exactly_one_of :beer, :wine, :grapefruit + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-array' do - end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit, type: Grape::API::Boolean - exactly_one_of :beer, :wine, :grapefruit - end + context 'when a subset of params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,grapefruit' => ['are mutually exclusive'] + ) end end - post '/deeply-nested-array' do - end - end - end - - describe '#validate!' do - subject(:validate) { post path, params } - context 'when all params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + context 'when exacly one param is present' do + let(:path) { '/' } + let(:params) { { beer: true, somethingelse: true } } - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] - ) + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end end - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } + context 'when none of the params are present' do + let(:path) { '/' } + let(:params) { { somethingelse: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] + 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] ) end end end - context 'when a subset of params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,grapefruit' => ['are mutually exclusive'] - ) + params do + optional :beer + optional :wine + optional :grapefruit + optional :other + exactly_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + end + end end - end - context 'when custom message is specified' do - let(:path) { '/custom-message' } - let(:params) { { beer: true, wine: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine' => ['you should choose one'] + 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end - context 'when exacly one param is present' do - let(:path) { '/' } - let(:params) { { beer: true, somethingelse: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' + end + post '/custom-message' do + end + end end - end - context 'when none of the params are present' do - let(:path) { '/' } - let(:params) { { somethingelse: true } } + let(:path) { '/custom-message' } + let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] + 'beer,wine' => ['you should choose one'] ) end end - context 'when params are nested inside required hash' do + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-hash' do + end + end + end + let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } @@ -172,7 +165,26 @@ end end - context 'when params are nested inside optional hash' do + describe '/nested-optional-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-optional-hash' do + end + end + end + let(:path) { '/nested-optional-hash' } context 'when params are passed' do @@ -197,7 +209,26 @@ end end - context 'when params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer + optional :wine + optional :grapefruit + exactly_one_of :beer, :wine, :grapefruit + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } @@ -215,7 +246,26 @@ end end - context 'when params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + exactly_one_of :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } diff --git a/spec/grape/validations/validators/length_validator_spec.rb b/spec/grape/validations/validators/length_validator_spec.rb index 7e85b4dd8..4021c6aa3 100644 --- a/spec/grape/validations/validators/length_validator_spec.rb +++ b/spec/grape/validations/validators/length_validator_spec.rb @@ -1,113 +1,17 @@ # frozen_string_literal: true describe Grape::Validations::Validators::LengthValidator do - let_it_be(:app) do - Class.new(Grape::API) do - params do - requires :list, length: { min: 2, max: 3 } - end - post 'with_min_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2 } - end - post 'with_min_only' do - end - - params do - requires :list, type: [Integer], length: { max: 3 } - end - post 'with_max_only' do - end - - params do - requires :list, type: Integer, length: { max: 3 } - end - post 'type_is_not_array' do - end - - params do - requires :list, type: Hash, length: { max: 3 } - end - post 'type_supports_length' do - end - - params do - requires :list, type: [Integer], length: { min: -3 } - end - post 'negative_min' do - end - - params do - requires :list, type: [Integer], length: { max: -3 } - end - post 'negative_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2.5 } - end - post 'float_min' do - end - - params do - requires :list, type: [Integer], length: { max: 2.5 } - end - post 'float_max' do - end - - params do - requires :list, type: [Integer], length: { min: 15, max: 3 } - end - post 'min_greater_than_max' do - end - - params do - requires :list, type: [Integer], length: { min: 3, max: 3 } - end - post 'min_equal_to_max' do - end - - params do - requires :list, type: [JSON], length: { min: 0 } - end - post 'zero_min' do - end - - params do - requires :list, type: [JSON], length: { max: 0 } - end - post 'zero_max' do - end - - params do - requires :list, type: [Integer], length: { min: 2, message: 'not match' } - end - post '/custom-message' do - end - - params do - requires :code, length: { is: 2 } - end - post 'is' do - end - - params do - requires :code, length: { is: -2 } - end - post 'negative_is' do - end - - params do - requires :code, length: { is: 2, max: 10 } - end - post 'is_with_max' do + describe '/with_min_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, length: { min: 2, max: 3 } + end + post 'with_min_max' do + end end end - end - describe '/with_min_max' do context 'when length is within limits' do it do post '/with_min_max', list: [1, 2] @@ -134,6 +38,16 @@ end describe '/with_max_only' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: 3 } + end + post 'with_max_only' do + end + end + end + context 'when length is less than limits' do it do post '/with_max_only', list: [1, 2] @@ -152,6 +66,16 @@ end describe '/with_min_only' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2 } + end + post 'with_min_only' do + end + end + end + context 'when length is greater than limit' do it do post '/with_min_only', list: [1, 2] @@ -170,6 +94,16 @@ end describe '/zero_min' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [JSON], length: { min: 0 } + end + post 'zero_min' do + end + end + end + context 'when length is equal to the limit' do it do post '/zero_min', list: '[]' @@ -188,6 +122,16 @@ end describe '/zero_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [JSON], length: { max: 0 } + end + post 'zero_max' do + end + end + end + context 'when length is within the limit' do it do post '/zero_max', list: '[]' @@ -206,6 +150,16 @@ end describe '/type_is_not_array' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: Integer, length: { max: 3 } + end + post 'type_is_not_array' do + end + end + end + context 'does not raise an error' do it do expect do @@ -216,6 +170,16 @@ end describe '/type_supports_length' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: Hash, length: { max: 3 } + end + post 'type_supports_length' do + end + end + end + context 'when length is within limits' do it do post 'type_supports_length', list: { key: 'value' } @@ -235,45 +199,105 @@ describe '/negative_min' do context 'when min is negative' do - it do - expect { post 'negative_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: -3 } + end + post 'negative_min' do + end + end + end + + it 'raises an error' do + expect { app }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end end end describe '/negative_max' do context 'it raises an error' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: -3 } + end + post 'negative_max' do + end + end + end + it do - expect { post 'negative_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') + expect { app }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end end end describe '/float_min' do context 'when min is not an integer' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2.5 } + end + post 'float_min' do + end + end + end + it do - expect { post 'float_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') + expect { app }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end end end describe '/float_max' do context 'when max is not an integer' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { max: 2.5 } + end + post 'float_max' do + end + end + end + it do - expect { post 'float_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') + expect { app }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end end end describe '/min_greater_than_max' do context 'raises an error' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 15, max: 3 } + end + post 'min_greater_than_max' do + end + end + end + it do - expect { post 'min_greater_than_max', list: [1, 2] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') + expect { app }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') end end end describe '/min_equal_to_max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 3, max: 3 } + end + post 'min_equal_to_max' do + end + end + end + context 'when array meets expectations' do it do post 'min_equal_to_max', list: [1, 2, 3] @@ -300,6 +324,16 @@ end describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + params do + requires :list, type: [Integer], length: { min: 2, message: 'not match' } + end + post '/custom-message' do + end + end + end + context 'is within limits' do it do post '/custom-message', list: [1, 2, 3] @@ -318,6 +352,16 @@ end describe '/is' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: 2 } + end + post 'is' do + end + end + end + context 'when length is exact' do it do post 'is', code: 'ZZ' @@ -352,17 +396,37 @@ end describe '/negative_is' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: -2 } + end + post 'negative_is' do + end + end + end + context 'when `is` is negative' do it do - expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero') + expect { app }.to raise_error(ArgumentError, 'is must be an integer greater than zero') end end end describe '/is_with_max' do context 'when `is` is combined with max' do + let(:app) do + Class.new(Grape::API) do + params do + requires :code, length: { is: 2, max: 10 } + end + post 'is_with_max' do + end + end + end + it do - expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max') + expect { app }.to raise_error(ArgumentError, 'is cannot be combined with min or max') end end end diff --git a/spec/grape/validations/validators/mutually_exclusive_spec.rb b/spec/grape/validations/validators/mutually_exclusive_spec.rb index a2c82e332..e217cb365 100644 --- a/spec/grape/validations/validators/mutually_exclusive_spec.rb +++ b/spec/grape/validations/validators/mutually_exclusive_spec.rb @@ -1,92 +1,85 @@ # frozen_string_literal: true describe Grape::Validations::Validators::MutuallyExclusiveValidator do - let_it_be(:app) do - Class.new(Grape::API) do - rescue_from Grape::Exceptions::ValidationErrors do |e| - error!(e.errors.transform_keys! { |key| key.join(',') }, 400) - end + describe '#validate!' do + subject(:validate) { post path, params } - params do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit - end - post do - end + describe '/' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - optional :beer - optional :wine - optional :grapefruit - optional :other - mutually_exclusive :beer, :wine, :grapefruit - end - post 'mixed-params' do + params do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + post do + end + end end - params do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' - end - post '/custom-message' do - end + context 'when all mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, wine: true, grapefruit: true } } - params do - requires :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,wine,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-hash' do - end - params do - optional :item, type: Hash do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + context 'when a subset of mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, grapefruit: true } } + + it 'returns a validation error' do + validate + expect(last_response.status).to eq 400 + expect(JSON.parse(last_response.body)).to eq( + 'beer,grapefruit' => ['are mutually exclusive'] + ) end end - post '/nested-optional-hash' do - end - params do - requires :items, type: Array do - optional :beer - optional :wine - optional :grapefruit - mutually_exclusive :beer, :wine, :grapefruit + context 'when no mutually exclusive params are present' do + let(:path) { '/' } + let(:params) { { beer: true, somethingelse: true } } + + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - post '/nested-array' do - end + end + + describe '/mixed-params' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - params do - requires :items, type: Array do - requires :nested_items, type: Array do - optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + params do + optional :beer + optional :wine + optional :grapefruit + optional :other mutually_exclusive :beer, :wine, :grapefruit end + post 'mixed-params' do + end end end - post '/deeply-nested-array' do - end - end - end - describe '#validate!' do - subject(:validate) { post path, params } - - context 'when all mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, wine: true, grapefruit: true } } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate @@ -95,35 +88,26 @@ 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end - - context 'mixed with other params' do - let(:path) { '/mixed-params' } - let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } - - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,wine,grapefruit' => ['are mutually exclusive'] - ) - end - end end - context 'when a subset of mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, grapefruit: true } } + describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'returns a validation error' do - validate - expect(last_response.status).to eq 400 - expect(JSON.parse(last_response.body)).to eq( - 'beer,grapefruit' => ['are mutually exclusive'] - ) + params do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' + end + post '/custom-message' do + end + end end - end - context 'when custom message is specified' do let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } @@ -136,17 +120,26 @@ end end - context 'when no mutually exclusive params are present' do - let(:path) { '/' } - let(:params) { { beer: true, somethingelse: true } } + describe '/nested-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end - it 'does not return a validation error' do - validate - expect(last_response.status).to eq 201 + params do + requires :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-hash' do + end + end end - end - context 'when mutually exclusive params are nested inside required hash' do let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } @@ -159,7 +152,26 @@ end end - context 'when mutually exclusive params are nested inside optional hash' do + describe '/nested-optional-hash' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :item, type: Hash do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-optional-hash' do + end + end + end + let(:path) { '/nested-optional-hash' } context 'when params are passed' do @@ -184,7 +196,26 @@ end end - context 'when mutually exclusive params are nested inside array' do + describe '/nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + optional :beer + optional :wine + optional :grapefruit + mutually_exclusive :beer, :wine, :grapefruit + end + end + post '/nested-array' do + end + end + end + let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } @@ -198,7 +229,26 @@ end end - context 'when mutually exclusive params are deeply nested' do + describe '/deeply-nested-array' do + let(:app) do + Class.new(Grape::API) do + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Grape::API::Boolean + mutually_exclusive :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end + end + end + let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } diff --git a/spec/grape/validations/validators/regexp_validator_spec.rb b/spec/grape/validations/validators/regexp_validator_spec.rb index c12e8a60a..1976f289c 100644 --- a/spec/grape/validations/validators/regexp_validator_spec.rb +++ b/spec/grape/validations/validators/regexp_validator_spec.rb @@ -1,66 +1,148 @@ # frozen_string_literal: true describe Grape::Validations::Validators::RegexpValidator do - let_it_be(:app) do - Class.new(Grape::API) do - default_format :json + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json - resources :custom_message do params do - requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } - end - get do + requires :name, regexp: { value: /^[a-z]+$/ } end + get '/bad_encoding' + end + end + + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error + end + end + end + + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json params do - requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + requires :name, regexp: /^[a-z]+$/ end - get 'regexp_with_array' do + get do end end + end - params do - requires :name, regexp: /^[a-z]+$/ - end - get do + 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 - params do - requires :names, type: Array[String], regexp: /^[a-z]+$/ - end - get 'regexp_with_array' do + 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 + + it 'accepts nil' do + get '/', name: nil + expect(last_response.status).to eq(200) + end + + it 'accepts valid input' do + get '/', name: 'bob' + expect(last_response.status).to eq(200) + end + end - params do - requires :people, type: Hash do + describe '/regexp_with_array' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do requires :names, type: Array[String], regexp: /^[a-z]+$/ end + get 'regexp_with_array' do + end end - get 'nested_regexp_with_array' do - end + end + + it 'refuses inapppopriate items' do + get '/regexp_with_array', names: ['invalid name', 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'refuses empty items' do + get '/regexp_with_array', names: ['', 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'refuses nil items' do + get '/regexp_with_array', names: [nil, 'abc'] + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"names is invalid"}') + end + + it 'accepts valid items' do + get '/regexp_with_array', names: ['bob'] + expect(last_response.status).to eq(200) + end + + it 'accepts nil instead of array' do + get '/regexp_with_array', names: nil + expect(last_response.status).to eq(200) end end - describe '#bad encoding' do + describe '/nested_regexp_with_array' do let(:app) do Class.new(Grape::API) do default_format :json params do - requires :name, regexp: { value: /^[a-z]+$/ } + requires :people, type: Hash do + requires :names, type: Array[String], regexp: /^[a-z]+$/ + end + end + get 'nested_regexp_with_array' do end - get '/bad_encoding' end end - context 'when value as bad encoding' do - it 'does not raise an error' do - expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error - end + it 'refuses inapppopriate' do + get '/nested_regexp_with_array', people: 'invalid name' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') end end - context 'custom validation message' do + describe '/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + resources :custom_message do + params do + requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + end + get do + end + + params do + requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } + end + get 'regexp_with_array' do + end + end + end + end + context 'with invalid input' do it 'refuses inapppopriate' do get '/custom_message', name: 'invalid name' @@ -115,66 +197,4 @@ end 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 - - it 'accepts nil' do - get '/', name: nil - expect(last_response.status).to eq(200) - end - - it 'accepts valid input' do - get '/', name: 'bob' - expect(last_response.status).to eq(200) - end - - context 'regexp with array' do - it 'refuses inapppopriate items' do - get '/regexp_with_array', names: ['invalid name', 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'refuses empty items' do - get '/regexp_with_array', names: ['', 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'refuses nil items' do - get '/regexp_with_array', names: [nil, 'abc'] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"names is invalid"}') - end - - it 'accepts valid items' do - get '/regexp_with_array', names: ['bob'] - expect(last_response.status).to eq(200) - end - - it 'accepts nil instead of array' do - get '/regexp_with_array', names: nil - expect(last_response.status).to eq(200) - end - end - - context 'nested regexp with array' do - it 'refuses inapppopriate' do - get '/nested_regexp_with_array', people: 'invalid name' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') - end - end end diff --git a/spec/grape/validations/validators/same_as_validator_spec.rb b/spec/grape/validations/validators/same_as_validator_spec.rb index cc1ac984e..62016e5fa 100644 --- a/spec/grape/validations/validators/same_as_validator_spec.rb +++ b/spec/grape/validations/validators/same_as_validator_spec.rb @@ -1,25 +1,18 @@ # frozen_string_literal: true describe Grape::Validations::Validators::SameAsValidator do - let_it_be(:app) do - Class.new(Grape::API) do - params do - requires :password - requires :password_confirmation, same_as: :password - end - post do - end - - params do - requires :password - requires :password_confirmation, same_as: { value: :password, message: 'not match' } - end - post '/custom-message' do + describe '/' do + let(:app) do + Class.new(Grape::API) do + params do + requires :password + requires :password_confirmation, same_as: :password + end + post do + end end end - end - describe '/' do context 'is the same' do it do post '/', password: '987654', password_confirmation: '987654' @@ -38,6 +31,17 @@ end describe '/custom-message' do + let(:app) do + Class.new(Grape::API) do + params do + requires :password + requires :password_confirmation, same_as: { value: :password, message: 'not match' } + end + post '/custom-message' do + end + end + end + context 'is the same' do it do post '/custom-message', password: '987654', password_confirmation: '987654' diff --git a/spec/grape/validations/validators/values_validator_spec.rb b/spec/grape/validations/validators/values_validator_spec.rb index 437202ac8..39f72d9b8 100644 --- a/spec/grape/validations/validators/values_validator_spec.rb +++ b/spec/grape/validations/validators/values_validator_spec.rb @@ -45,384 +45,453 @@ def default_excepts end end - let(:app) do - Class.new(Grape::API) do - default_format :json + before do + stub_const('ValuesModel', values_model) + end - 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 + describe '#bad encoding' do + let(:app) do + Class.new(Grape::API) do + default_format :json 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] } + requires :type, type: String, values: %w[a b] end + get '/bad_encoding' end + end - params do - requires :type, values: ValuesModel.values - end - get '/' do - { type: params[:type] } + context 'when value as bad encoding' do + it 'does not raise an error' do + expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error end + end + end - params do - requires :type, values: [] - end - get '/empty' + describe '/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' - end - get '/default/hash/valid' do - { type: params[:type] } + 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 end + end - params do - optional :type, values: ValuesModel.values, default: 'valid-type2' - end - get '/default/valid' do - { type: params[:type] } - end + 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 - params do - optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' - end - get '/lambda' do - { type: params[:type] } - 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 - params do - optional :type, type: Integer, values: 1.. - end - get '/endless' do - { type: params[:type] } - end + it 'validates against values in a proc' do + ValuesModel.add_value('valid-type4') - params do - requires :type, values: ->(v) { ValuesModel.include? v } - end - get '/lambda_val' do - { type: params[:type] } - end + 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 - params do - requires :number, type: Integer, values: ->(v) { v > 0 } - end - get '/lambda_int_val' do - { number: params[:number] } - 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 - params do - requires :type, values: -> { [] } - end - get '/empty_lambda' + describe '/' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } - end - get '/default_lambda' do - { type: params[:type] } + params do + requires :type, values: ValuesModel.values + end + get '/' do + { type: params[:type] } + end end + end - params do - optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } - end - get '/default_and_values_lambda' do - { type: params[:type] } - end + it 'allows a valid value for a parameter' do + get('/', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) + end - params do - optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] - end - get '/values/optional_boolean' do - { type: params[:type] } - end + it 'does not allow an invalid value for a parameter' do + get('/', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end - params do - requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/coercion' do - { type: params[:type] } + context 'nil value for a parameter' do + it 'does not allow for root params scope' do + get('/', type: nil) + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end - params do - requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 - end - get '/values/array_coercion' do - { type: params[:type] } - end + it 'does not validate updated values without proc' do + app # Instantiate with the existing values. + ValuesModel.add_value('valid-type4') + get('/', type: 'valid-type4') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + end - params do - optional :optional, type: Array do - requires :type, values: %w[a b] + describe '/empty' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: [] end + get '/empty' end - get '/optional_with_required_values' + end - params do - requires :type, type: Integer, values: 1..5, except_values: [3] - end - get '/mixed/value/except' do - { type: params[:type] } - end + it 'rejects all values if values is an empty array' do + get('/empty', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end + end - params do - optional :optional, type: Array[String], values: %w[a b c] - end - put '/optional_with_array_of_string_values' + describe '/optional_with_required_values' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :type, values: ->(v) { ValuesModel.include? v } - end - get '/proc' do - { type: params[:type] } + params do + optional :optional, type: Array do + requires :type, values: %w[a b] + end + end + get '/optional_with_required_values' end + end - params do - requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } - end - get '/proc/message' + it 'allows nil value for a required param in child scope' do + get('/optional_with_required_values') + expect(last_response.status).to eq 200 + end + end - params do - requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } - end - get '/proc/custom_message' do - { message: 'success' } - end + describe '/optional_with_array_of_string_values' do + let(:app) do + Class.new(Grape::API) do + default_format :json - params do - requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } + params do + optional :optional, type: Array[String], values: %w[a b c] + end + put '/optional_with_array_of_string_values' end - get '/proc/arity2' + end - params do - optional :name, type: String, values: %w[a b], allow_blank: true - end - get '/allow_blank' + it 'accepts nil for an optional param with a list of values' do + put('/optional_with_array_of_string_values', optional: nil) + expect(last_response.status).to eq 200 + end + end - params do - with(type: String) do - requires :type, values: ValuesModel.values + describe '/default/valid' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, values: ValuesModel.values, default: 'valid-type2' + end + get '/default/valid' do + { type: params[:type] } end end - get 'values_wrapped_by_with_block' end - end - before do - stub_const('ValuesModel', values_model) + it 'allows a valid default value' do + get('/default/valid') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) + end end - describe '#bad encoding' do + describe '/default/hash/valid' do let(:app) do Class.new(Grape::API) do default_format :json params do - requires :type, type: String, values: %w[a b] + optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' + end + get '/default/hash/valid' do + { type: params[:type] } end - get '/bad_encoding' end end - context 'when value as bad encoding' do - it 'does not raise an error' do - expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error - end + it 'allows a valid default value' do + get('/default/hash/valid') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end 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) + describe '/lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' + end + get '/lambda' do + { type: params[:type] } + end + end 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) + it 'allows a proc for values' do + get('/lambda', type: 'valid-type1') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'validates against values in a proc' do ValuesModel.add_value('valid-type4') - get('/custom_message/lambda', type: 'valid-type4') + get('/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') + get('/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) + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end - it 'allows a valid value for a parameter' do - get('/', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) - end + describe '/endless' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'does not allow an invalid value for a parameter' do - get('/', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + params do + optional :type, type: Integer, values: 1.. + end + get '/endless' do + { type: params[:type] } + end + end + end - it 'rejects all values if values is an empty array' do - get('/empty', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + it 'validates against values in an endless range' do + get('/endless', type: 10) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 10 }.to_json) + end - context 'nil value for a parameter' do - it 'does not allow for root params scope' do - get('/', type: nil) + it 'does not allow an invalid value for a parameter using an endless range' do + get('/endless', type: 0) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end - it 'allows for a required param in child scope' do - get('/optional_with_required_values') - expect(last_response.status).to eq 200 + describe '/lambda_val' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: ->(v) { ValuesModel.include? v } + end + get '/lambda_val' do + { type: params[:type] } + end + end end - it 'accepts for an optional param with a list of values' do - put('/optional_with_array_of_string_values', optional: nil) + it 'allows value using lambda' do + get('/lambda_val', type: 'valid-type1') expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end - end - it 'allows a valid default value' do - get('/default/valid') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) + it 'does not allow invalid value using lambda' do + get('/lambda_val', type: 'invalid-type') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end end - it 'allows a valid default value' do - get('/default/hash/valid') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) - end + describe '/lambda_int_val' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows a proc for values' do - get('/lambda', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) - end + params do + requires :number, type: Integer, values: ->(v) { v > 0 } + end + get '/lambda_int_val' do + { number: params[:number] } + end + end + end - it 'does not validate updated values without proc' do - app # Instantiate with the existing values. - ValuesModel.add_value('valid-type4') - get('/', type: 'valid-type4') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + it 'does not allow non-numeric string value for int value using lambda' do + get('/lambda_int_val', number: 'foo') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) + end - it 'validates against values in a proc' do - ValuesModel.add_value('valid-type4') + it 'does not allow nil for int value using lambda' do + get('/lambda_int_val', number: nil) + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) + end - get('/lambda', type: 'valid-type4') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) + it 'allows numeric string for int value using lambda' do + get('/lambda_int_val', number: '3') + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ number: 3 }.to_json) + end end - it 'does not allow an invalid value for a parameter using lambda' do - get('/lambda', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + describe '/empty_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'validates against values in an endless range' do - get('/endless', type: 10) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 10 }.to_json) - end + params do + requires :type, values: -> { [] } + end + get '/empty_lambda' + end + end - it 'does not allow an invalid value for a parameter using an endless range' do - get('/endless', type: 0) - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + it 'validates against an empty array in a proc' do + get('/empty_lambda', type: 'any') + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + end end - it 'does not allow non-numeric string value for int value using lambda' do - get('/lambda_int_val', number: 'foo') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) - end + describe '/default_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'does not allow nil for int value using lambda' do - get('/lambda_int_val', number: nil) - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) - end + params do + optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } + end + get '/default_lambda' do + { type: params[:type] } + end + end + end - it 'allows numeric string for int value using lambda' do - get('/lambda_int_val', number: '3') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ number: 3 }.to_json) + it 'validates default value from proc' do + get('/default_lambda') + expect(last_response.status).to eq 200 + end end - it 'allows value using lambda' do - get('/lambda_val', type: 'valid-type1') - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) - end + describe '/default_and_values_lambda' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'does not allow invalid value using lambda' do - get('/lambda_val', type: 'invalid-type') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) - end + params do + optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } + end + get '/default_and_values_lambda' do + { type: params[:type] } + end + end + end - it 'validates against an empty array in a proc' do - get('/empty_lambda', type: 'any') - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) + it 'validates default value from proc against values in a proc' do + get('/default_and_values_lambda') + expect(last_response.status).to eq 200 + end end - it 'validates default value from proc' do - get('/default_lambda') - expect(last_response.status).to eq 200 - end + context 'IncompatibleOptionValues' do + it 'raises on an invalid default value from proc' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'validates default value from proc against values in a proc' do - get('/default_and_values_lambda') - expect(last_response.status).to eq 200 - end + it 'raises on an invalid default value' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues on an invalid default value from proc' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end + it 'raises when type is incompatible with values array' do + subject = Class.new(Grape::API) + expect do + subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues on an invalid default value' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues - end + it 'raises when values contains a value that is not a kind of the type' do + subject = Class.new(Grape::API) + expect do + subject.params { requires :type, values: [10.5, 11], type: Integer } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end - it 'raises IncompatibleOptionValues when type is incompatible with values array' do - subject = Class.new(Grape::API) - expect do - subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + it 'raises when except contains a value that is not a kind of the type' do + subject = Class.new(Grape::API) + expect do + subject.params { requires :type, except_values: [10.5, 11], type: Integer } + end.to raise_error Grape::Exceptions::IncompatibleOptionValues + end end - context 'boolean values' do + describe '/values/optional_boolean' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] + end + get '/values/optional_boolean' do + { type: params[:type] } + end + end + end + it 'allows a value from the list' do get('/values/optional_boolean', type: true) @@ -437,38 +506,67 @@ def default_excepts end end - it 'allows values to be a kind of the coerced type not just an instance of it' do - get('/values/coercion', type: 10) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: 10 }.to_json) - end + describe '/values/coercion' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'allows values to be a kind of the coerced type in an array' do - get('/values/array_coercion', type: [10]) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: [10] }.to_json) - end + params do + requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/coercion' do + { type: params[:type] } + end + end + end - it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do - subject = Class.new(Grape::API) - expect do - subject.params { requires :type, values: [10.5, 11], type: Integer } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + it 'allows values to be a kind of the coerced type not just an instance of it' do + get('/values/coercion', type: 10) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: 10 }.to_json) + end end - it 'raises IncompatibleOptionValues when except contains a value that is not a kind of the type' do - subject = Class.new(Grape::API) - expect do - subject.params { requires :type, except_values: [10.5, 11], type: Integer } - end.to raise_error Grape::Exceptions::IncompatibleOptionValues + describe '/values/array_coercion' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 + end + get '/values/array_coercion' do + { type: params[:type] } + end + end + end + + it 'allows values to be a kind of the coerced type in an array' do + get('/values/array_coercion', type: [10]) + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ type: [10] }.to_json) + end end - it 'allows a blank value when the allow_blank option is true' do - get 'allow_blank', name: nil - expect(last_response.status).to eq(200) + describe '/allow_blank' do + let(:app) do + Class.new(Grape::API) do + default_format :json - get 'allow_blank', name: '' - expect(last_response.status).to eq(200) + params do + optional :name, type: String, values: %w[a b], allow_blank: true + end + get '/allow_blank' + end + end + + it 'allows a blank value when the allow_blank option is true' do + get 'allow_blank', name: nil + expect(last_response.status).to eq(200) + + get 'allow_blank', name: '' + expect(last_response.status).to eq(200) + end end context 'with a lambda values' do @@ -544,7 +642,20 @@ def app end end - context 'with mixed values and excepts' do + describe '/mixed/value/except' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, type: Integer, values: 1..5, except_values: [3] + end + get '/mixed/value/except' do + { type: params[:type] } + end + end + end + it 'allows value, but not in except' do get '/mixed/value/except', type: 2 expect(last_response.status).to eq 200 @@ -564,7 +675,20 @@ def app end end - context 'custom validation using proc' do + describe '/proc' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: ->(v) { ValuesModel.include? v } + end + get '/proc' do + { type: params[:type] } + end + end + end + it 'accepts a single valid value' do get '/proc', type: 'valid-type1' expect(last_response.status).to eq 200 @@ -588,36 +712,85 @@ def app expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end + end + + describe '/proc/message' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } + end + get '/proc/message' + end + end it 'uses supplied message' do get '/proc/message', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type failed check' }.to_json) end + end - context 'when proc has an arity of 1' do - it 'accepts a valid value' do - get '/proc/custom_message', number: 4 - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ message: 'success' }.to_json) - end + describe '/proc/custom_message' do + let(:app) do + Class.new(Grape::API) do + default_format :json - it 'rejects an invalid value' do - get '/proc/custom_message', number: 5 - expect(last_response.status).to eq 400 - expect(last_response.body).to eq({ error: 'number must be even' }.to_json) + params do + requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } + end + get '/proc/custom_message' do + { message: 'success' } + end end end - context 'when arity is > 1' do - it 'returns an error status code' do - get '/proc/arity2', input_one: 2, input_two: 3 - expect(last_response.status).to eq 400 + it 'accepts a valid value' do + get '/proc/custom_message', number: 4 + expect(last_response.status).to eq 200 + expect(last_response.body).to eq({ message: 'success' }.to_json) + end + + it 'rejects an invalid value' do + get '/proc/custom_message', number: 5 + expect(last_response.status).to eq 400 + expect(last_response.body).to eq({ error: 'number must be even' }.to_json) + end + end + + describe '/proc/arity2' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } + end + get '/proc/arity2' end end + + it 'returns an error status code' do + expect { app }.to raise_error(ArgumentError, 'values Proc must have arity of zero or one') + end end - context 'when wrapped by with block' do + describe '/values_wrapped_by_with_block' do + let(:app) do + Class.new(Grape::API) do + default_format :json + + params do + with(type: String) do + requires :type, values: ValuesModel.values + end + end + get 'values_wrapped_by_with_block' + end + end + it 'rejects an invalid value' do get 'values_wrapped_by_with_block' diff --git a/spec/grape/validations/validators/zh-CN.yml b/spec/grape/validations/validators/zh-CN.yml index 39f6bbd7f..0dcd06a25 100644 --- a/spec/grape/validations/validators/zh-CN.yml +++ b/spec/grape/validations/validators/zh-CN.yml @@ -8,3 +8,4 @@ zh-CN: coerce: '格式不正确' presence: '请填写' regexp: '格式不正确' + same_as: '与%{parameter}不一致' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 358f002b1..abcaa2146 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ Grape.deprecator.behavior = :raise -%w[config support].each do |dir| +%w[support].each do |dir| Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].each do |file| require file end