Skip to content

Generalize middleware options to per-class Options Data value objects#2718

Open
ericproulx wants to merge 1 commit into
masterfrom
draft/middleware-options-data
Open

Generalize middleware options to per-class Options Data value objects#2718
ericproulx wants to merge 1 commit into
masterfrom
draft/middleware-options-data

Conversation

@ericproulx
Copy link
Copy Markdown
Contributor

@ericproulx ericproulx commented May 14, 2026

Summary

Following the per-feature VersionOptions / RescueOptions work in #2712 and #2716, middlewares that want typed option accessors now declare a per-class Options = Data.define(…). Middleware::Base routes the kwargs through it on init and exposes both a frozen Hash (options, back-compat) and the typed Data instance (config).

Mechanism

Middleware::Base#initialize, when the subclass declares an Options constant (ancestor search included so subclasses like Versioner::Path inherit Versioner::Base::Options):

  • builds @config = self.class::Options.new(**options)
  • sets @options = @config.to_h.freeze (frozen Hash view of the Data, for legacy options[:key] callers)

Subclasses without an Options constant (Filter, Auth::*) continue to flow through the legacy DEFAULT_OPTIONS Hash + deep_merge path unchanged.

Converted middlewares

  • Middleware::Formatter (5 fields).
  • Middleware::Error (14 fields). rescue_options: defaults to Grape::DSL::RescueOptions.new; an explicit nil (passed by Endpoint#error_middleware_options when no rescue_from was called) is coerced to the default in the Data's initialize.
  • Middleware::Versioner::Base (7 fields). Adds content_types: / format: so the PrecomputedContentTypes mixin's accessor reads land cleanly. version_options: defaults to Grape::DSL::VersionOptions.new.

Each Options field list, def_delegators :config, and Options#initialize kwarg list is alphabetic.

Back-compat surface

  • attr_reader :options still returns a frozen Hash representation of the Data (config.to_h.freeze) for converted middlewares. Subclasses that read options[:key] keep working.
  • DEFAULT_OPTIONS is restored as Options.new.to_h.freeze on each converted middleware so existing references resolve. Marked @deprecated in the YARD comment; will be removed in a future release.
  • Options#[] is defined on each Data class as a Hash-style shim that emits a deprecation warning via Grape.deprecator and forwards to the named accessor. Any code that reaches into the Data with [:key] gets a migration nudge.

Contract change (documented in UPGRADING)

Passing an unknown kwarg to a converted middleware now raises ArgumentError instead of being silently swallowed by **options. One formatter spec was passing rescue_options: (dead weight; Formatter doesn't read it) — dropped.

That stricter contract is exactly what made version_options / rescue_options cleaner in their respective PRs.

Test plan

  • bundle exec rspec — 2313 examples, 0 failures
  • bundle exec rubocop lib/grape/middleware/ — clean
  • CI green

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx force-pushed the draft/middleware-options-data branch 2 times, most recently from e707766 to 13add91 Compare May 16, 2026 21:53
@ericproulx ericproulx force-pushed the draft/middleware-options-data branch 2 times, most recently from 6fc81d9 to b4ab849 Compare May 24, 2026 19:18
@ericproulx ericproulx changed the title [DRAFT] Generalize middleware options to per-class Data value objects Generalize middleware options to per-class Options Data value objects May 24, 2026
@ericproulx ericproulx marked this pull request as ready for review May 24, 2026 19:18
Following the per-feature `VersionOptions` / `RescueOptions` work in #2712
and #2716, middlewares that want typed option accessors now declare a
per-class `Options = Data.define(...)`.

`Middleware::Base#initialize`, when the subclass declares an `Options`
constant (ancestor search included), routes the kwargs through
`Options.new(**options)`:

- `@config` exposes the typed Data instance (named accessors).
- `@options` keeps returning a frozen `Hash` representation
  (`config.to_h.freeze`) for back-compat with subclasses that read
  `options[:key]`.

Subclasses without an `Options` constant still flow through the
`DEFAULT_OPTIONS` Hash + `deep_merge` path (`Filter`, `Auth::*`) unchanged.

- `Middleware::Formatter` (5 fields).
- `Middleware::Error` (14 fields). `rescue_options:` defaults to
  `Grape::DSL::RescueOptions.new`; an explicit nil (passed by
  `Endpoint#error_middleware_options` when no `rescue_from` was called)
  is coerced to the default in the initializer.
- `Middleware::Versioner::Base` (7 fields). Adds `content_types:` /
  `format:` so the `PrecomputedContentTypes` mixin's accessor reads land
  cleanly. `version_options:` defaults to `Grape::DSL::VersionOptions.new`.

Each `Options` field list is alphabetic.

- `attr_reader :options` still returns a frozen `Hash` for converted
  middlewares (now derived from `config.to_h`). Subclasses that read
  `options[:key]` keep working.
- `DEFAULT_OPTIONS` is restored as `Options.new.to_h.freeze` on each
  converted middleware so existing references continue to resolve.
  Marked `@deprecated` in the YARD comment; will be removed in a future
  release.
- `Options#[]` is defined on each Data class as a Hash-style shim that
  emits a deprecation warning via `Grape.deprecator` and forwards to
  the named accessor — gives any code that discovers the Data and
  reaches in with `[:key]` a migration nudge.

Passing an unknown kwarg to a converted middleware now raises
`ArgumentError` instead of being silently swallowed by `**options`.
One formatter spec was passing `rescue_options:` (dead weight; Formatter
doesn't read it) — dropped.

That stricter contract is exactly what made `version_options` /
`rescue_options` cleaner in their respective PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the draft/middleware-options-data branch from b4ab849 to 99ee21a Compare May 24, 2026 19:22
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