diff --git a/CHANGELOG.md b/CHANGELOG.md index 42923f0d0..649d56741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * [#2712](https://github.com/ruby-grape/grape/pull/2712): Pass a `Grape::Exceptions::ErrorResponse` value object to `error_formatter#call` instead of separate kwargs - [@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). +* [#2722](https://github.com/ruby-grape/grape/pull/2722): Introduce `Grape::Validations::CoerceOptions` value object for the internal coerce options - [@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). * [#2719](https://github.com/ruby-grape/grape/pull/2719): Move content-type helpers from `Middleware::Base` into `PrecomputedContentTypes` - [@ericproulx](https://github.com/ericproulx). * [#2716](https://github.com/ruby-grape/grape/pull/2716): Refactor `DSL::Routing#version`: guard clause, explicit kwargs in place of `**options`, and a `Grape::DSL::VersionOptions` value object stored internally - [@ericproulx](https://github.com/ericproulx). diff --git a/lib/grape/validations/coerce_options.rb b/lib/grape/validations/coerce_options.rb new file mode 100644 index 000000000..274bfdd66 --- /dev/null +++ b/lib/grape/validations/coerce_options.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Grape + module Validations + # Immutable value object describing how a parameter is coerced. Assembled + # by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/ + # +coerce_message+ declaration — never written by the user — and consumed + # by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by + # {Validators::Validators::CoerceValidator} (which receives it as its + # +options+ argument). + # + # All three fields may be +nil+ (e.g. a remountable API evaluated on its + # base instance has no resolved +type+ yet). + # +coerce_method+ (not +method+) avoids shadowing +Object#method+. + CoerceOptions = Data.define(:type, :coerce_method, :message) do + def initialize(type: nil, coerce_method: nil, message: nil) + super + end + end + end +end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index ae3d8c17e..2d9722fe4 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -348,13 +348,13 @@ def validates(attrs, validations) end end - # Enforce correct usage of :coerce_with on a coerce_options hash. + # Enforce correct usage of :coerce_with on a CoerceOptions. # We do not allow coercion without a type, nor with +JSON+ as a type # since that defines its own coercion method. def check_coerce_with(coerce_options) - return unless coerce_options[:method] - raise ArgumentError, 'must supply type for coerce_with' unless coerce_options[:type] - return unless SPECIAL_JSON.include?(coerce_options[:type]) + return unless coerce_options.coerce_method + raise ArgumentError, 'must supply type for coerce_with' unless coerce_options.type + return unless SPECIAL_JSON.include?(coerce_options.type) raise ArgumentError, 'coerce_with disallowed for type: JSON' end @@ -373,7 +373,7 @@ def validate_coerce(spec, attrs) # configuration[:some_type] evaluates to nil. Skipping instantiation # here is correct — the real mounted instance will replay this step # with the actual type value. - return unless coerce_options[:type] + return unless coerce_options.type validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts) end diff --git a/lib/grape/validations/validations_spec.rb b/lib/grape/validations/validations_spec.rb index 95f2eb42e..ac0bbf11f 100644 --- a/lib/grape/validations/validations_spec.rb +++ b/lib/grape/validations/validations_spec.rb @@ -65,7 +65,7 @@ def required? end def coerce_options - { type: @coerce_type, method: @coerce_method, message: @coerce_message } + CoerceOptions.new(type: @coerce_type, coerce_method: @coerce_method, message: @coerce_message) end private diff --git a/lib/grape/validations/validators/coerce_validator.rb b/lib/grape/validations/validators/coerce_validator.rb index 222351c80..477bf5d6b 100644 --- a/lib/grape/validations/validators/coerce_validator.rb +++ b/lib/grape/validations/validators/coerce_validator.rb @@ -9,13 +9,18 @@ class CoerceValidator < Base def initialize(attrs, options, required, scope, opts) super - raw_type = @options[:type] + # +@options+ is a Grape::Validations::CoerceOptions. +Base#message+ + # can't see a custom message off a Data (it probes Hash-like + # +key?+), so restore it here to preserve `type: { value:, message: }`. + @exception_message = @options.message unless @options.message.nil? + + raw_type = @options.type type = hash_like?(raw_type) ? raw_type[:value] : raw_type @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) type else - Types.build_coercer(type, method: @options[:method]) + Types.build_coercer(type, method: @options.coerce_method) end end