From ae113282b1c48dbd387e3d4e08ba74113c0294f5 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 15 May 2026 10:59:10 +0200 Subject: [PATCH] Use an internal Grape::Validations::SharedOptions value object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Validators::Base#initialize` consumed the shared `opts` via `opts.values_at(:fail_fast, :allow_blank)` and copied the two values into per-validator ivars. Introduce `Grape::Validations::SharedOptions` (`Data.define(:allow_blank, :fail_fast)` with defaults mirroring the prior `values_at` behaviour) as an *internal* representation: `Base` builds it from the `opts` Hash — `SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))` — and exposes the two fields via `Forwardable.def_delegators :@opts, :allow_blank, :fail_fast`. `#fail_fast?` calls the delegated `fail_fast`; `ValuesValidator` (the one subclass reading `@allow_blank` directly) uses the `allow_blank` reader. The public 5th-argument contract of `Validators::Base#initialize` stays a plain Hash, so custom validators and any code that hand-constructs a validator with a Hash are unaffected. `slice` keeps the historical tolerance of ignoring unknown keys. `ValidationsSpec` keeps emitting the `{ allow_blank:, fail_fast: }.freeze` Hash; nothing about its public surface changes — no UPGRADING entry needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + lib/grape/validations/shared_options.rb | 19 +++++++++++++++++++ lib/grape/validations/validators/base.rb | 14 +++++++++++--- .../validators/values_validator.rb | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 lib/grape/validations/shared_options.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e3e6eca..3955f3cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * [#2710](https://github.com/ruby-grape/grape/pull/2710): Tidy up `Grape::DeclaredParamsHandler` - [@ericproulx](https://github.com/ericproulx). * [#2714](https://github.com/ruby-grape/grape/pull/2714): Drop unused `Grape::Middleware::Globals` and its `grape.request*` env constants - [@ericproulx](https://github.com/ericproulx). * [#2717](https://github.com/ruby-grape/grape/pull/2717): Convert `Grape::Exceptions::ErrorResponse` to a `Data` value object - [@ericproulx](https://github.com/ericproulx). +* [#2721](https://github.com/ruby-grape/grape/pull/2721): Use an internal `Grape::Validations::SharedOptions` value object in `Validators::Base` (public `opts` Hash contract unchanged) - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/validations/shared_options.rb b/lib/grape/validations/shared_options.rb new file mode 100644 index 000000000..c6f029739 --- /dev/null +++ b/lib/grape/validations/shared_options.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Grape + module Validations + # Immutable value object holding the two options every validator reads at + # construction time: +allow_blank+ and +fail_fast+. Internal to + # {Validators::Base}, which builds it from the +opts+ Hash so the public + # 5th-argument contract stays a plain Hash — not part of any wire contract. + # + # Defaults mirror the prior +opts.values_at+ behaviour: +allow_blank+ is + # +nil+ when the declaration didn't supply it (validators treat nil as + # "not set"), +fail_fast+ defaults to +false+. + SharedOptions = Data.define(:allow_blank, :fail_fast) do + def initialize(allow_blank: nil, fail_fast: false) + super + end + end + end +end diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 9d82f575c..2c290a9c1 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -12,10 +12,17 @@ module Validators # from them are frozen by construction. Lazy ivar assignment # (e.g. +memoize+, ||=) will raise +FrozenError+ at request time. class Base + extend Forwardable include Grape::Util::Translation attr_reader :attrs + # +allow_blank+ / +fail_fast+ are read straight off the internal + # {Grape::Validations::SharedOptions} value object; no per-validator + # ivars. The object is built from the +opts+ Hash in #initialize, so + # the public 5th-argument contract stays a plain Hash. + def_delegators :@opts, :allow_blank, :fail_fast + class << self # Declares the default I18n message key used by +validation_error!+. # Subclasses that only need a single fixed error message can declare it @@ -52,14 +59,15 @@ def inherited(klass) # @param options [Object] implementation-dependent Validator options; deep-frozen on assignment # @param required [Boolean] attribute(s) are required or optional # @param scope [ParamsScope] parent scope for this Validator - # @param opts [Hash] additional validation options + # @param opts [Hash] shared validator options; only +:allow_blank+ and + # +:fail_fast+ are consulted (other keys ignored, as before) def initialize(attrs, options, required, scope, opts) @attrs = Array(attrs).freeze @options = Grape::Util::DeepFreeze.deep_freeze(options) @option = @options # TODO: remove in next major release @required = required @scope = scope - @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank) + @opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast)) @exception_message = message(self.class.default_message_key) if self.class.default_message_key end @@ -75,7 +83,7 @@ def validate(request) end def fail_fast? - @fail_fast + fail_fast end # Validates a given parameter hash. diff --git a/lib/grape/validations/validators/values_validator.rb b/lib/grape/validations/validators/values_validator.rb index 15e766130..fa53f3bb5 100644 --- a/lib/grape/validations/validators/values_validator.rb +++ b/lib/grape/validations/validators/values_validator.rb @@ -28,7 +28,7 @@ def validate_param!(attr_name, params) val = scrub(params[attr_name]) return if val.nil? && !required_for_root_scope? - return if val != false && val.blank? && @allow_blank + return if val != false && val.blank? && allow_blank return if check_values?(val, attr_name) validation_error!(attr_name)