Skip to content

Introduce Grape::Validations::CoerceOptions value object#2722

Open
ericproulx wants to merge 1 commit into
masterfrom
refactor/coerce-options-data
Open

Introduce Grape::Validations::CoerceOptions value object#2722
ericproulx wants to merge 1 commit into
masterfrom
refactor/coerce-options-data

Conversation

@ericproulx
Copy link
Copy Markdown
Contributor

Summary

ValidationsSpec#coerce_options returned an ad-hoc { type:, method:, message: } Hash that flowed (deep-frozen) into CoerceValidator as its options argument and was poked at by ParamsScope#check_coerce_with / #validate_coerce.

Unlike most validator options (which are a genuine union — bare value, { value:, message: } envelope, or structured Hash, depending on what the user wrote), this one is never written by the user. It's assembled internally from the parsed type: / coerce_with: / coerce_message declaration, so it has a fixed shape and no public contract to preserve — the cleanest possible Data.define candidate.

Replace it with Grape::Validations::CoerceOptions:

CoerceOptions = Data.define(:type, :coerce_method, :message) do
  def initialize(type: nil, coerce_method: nil, message: nil)
    super
  end
end

The member is coerce_method, not method — defining :method would shadow Object#method (Lint/DataDefineOverride), and coerce_method already matches ValidationsSpec's existing accessor name.

Changes

  • ValidationsSpec#coerce_options builds the value object.
  • ParamsScope#check_coerce_with / #validate_coerce read .type / .coerce_method instead of [:type] / [:method].
  • CoerceValidator reads @options.type / @options.coerce_method. Base#message resolves a custom message by probing a Hash-like key?, which a Data doesn't answer, so CoerceValidator#initialize restores @exception_message from @options.message — preserving type: { value: Integer, message: 'bad' } behaviour (covered by existing specs).

Why it's safe

  • Grape::Util::DeepFreeze returns the Data via its else branch unchanged; Base.new already freezes the whole validator instance, and Data is structurally immutable.
  • No user-facing contract change: users never construct or read this Hash; no spec hand-builds a CoerceValidator with it (the validator specs go through the full Grape::API DSL).
  • The remountable-API return unless coerce_options.type guard in validate_coerce is preserved (a base instance with no resolved type still skips).

Test plan

  • bundle exec rspec — 2307 examples, 0 failures (incl. coerce_validator_spec 97/0, custom-message path)
  • RuboCop clean (incl. Lint/DataDefineOverride)
  • CI green across Gemfile variants

🤖 Generated with Claude Code

`ValidationsSpec#coerce_options` returned an ad-hoc
`{ type:, method:, message: }` Hash that flowed (deep-frozen) into
`CoerceValidator` as its `options` argument and was poked at by
`ParamsScope#check_coerce_with` / `#validate_coerce`. Unlike most
validator options, this Hash is *never written by the user* — it is
assembled internally from the parsed `type:` / `coerce_with:` /
`coerce_message` declaration — so it has a fixed shape and no public
contract to preserve.

Replace it with a `Grape::Validations::CoerceOptions` Data value object
(`Data.define(:type, :coerce_method, :message)`, all defaulting nil).
The member is `coerce_method`, not `method`, to avoid shadowing
`Object#method` (`Lint/DataDefineOverride`); it also matches
`ValidationsSpec`'s existing `coerce_method` vocabulary.

- `ValidationsSpec#coerce_options` builds the value object.
- `ParamsScope#check_coerce_with` / `#validate_coerce` read `.type` /
  `.coerce_method` instead of `[:type]` / `[:method]`.
- `CoerceValidator` reads `@options.type` / `@options.coerce_method`.
  `Base#message` can't see a custom message off a Data (it probes
  Hash-like `key?`), so `CoerceValidator#initialize` restores
  `@exception_message` from `@options.message`, preserving
  `type: { value:, message: }` behaviour.

`Grape::Util::DeepFreeze` returns the Data as-is (its `else` branch);
`Base.new` already freezes the whole validator instance, and the Data
is structurally immutable. No user-facing contract changes — users
never construct or read this Hash; no spec hand-builds it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the refactor/coerce-options-data branch from 90b651e to 985d739 Compare May 15, 2026 09:25
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx requested a review from dblock May 15, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant