Module: ConcernsOnRails::Controllers::Deprecatable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/concerns_on_rails/controllers/deprecatable.rb
Overview
Standards-based API endpoint deprecation — the receiving end of an API retirement plan (how Stripe / GitHub / Zalando sunset versions). Declare which actions are deprecated and the standard signalling headers go out on every response so clients (and their SDKs) can discover the deprecation, the migration docs, the successor endpoint, and the hard cut-off — then optionally enforce a 410 Gone once that cut-off passes.
class Api::V1::OrdersController < ApplicationController
include ConcernsOnRails::Controllers::Deprecatable
deprecate_actions :index, :show,
deprecated_at: "2026-06-01",
sunset_at: "2026-12-31T00:00:00Z",
link: "https://docs.example.com/v1-migration",
successor: "https://api.example.com/v2/orders",
after_sunset: :gone,
notify: -> { StatsD.increment("api.v1.orders.deprecated") }
end
Headers emitted (always — including on the 410, so the failure is self-documenting):
* Deprecation — RFC 9745. The final form is a structured-fields Date
item: "@<unix-seconds>" of `deprecated_at`. `header_format: :legacy`
emits the still-widely-deployed pre-RFC draft form, the literal "true".
* Sunset — RFC 8594. An IMF-fixdate (HTTP-date) via Time#httpdate,
NOT ISO 8601 — hand-rolling that is the classic bug.
* Link — RFC 8288. rel="deprecation" (the migration doc) and/or
rel="successor-version" (the replacement endpoint), APPENDED to any
Link header already on the response (pagination / CDN), never clobbered.
‘sunset_at` is an INSTANT, not a calendar day: a bare date “2026-12-31” is normalised to 00:00 UTC, so the endpoint dies at the START of that day, not end-of-day. Times are parsed eagerly at declaration time and normalised to UTC.
THE LAST MATCHING RULE WINS (deliberately the reverse of Idempotentable’s first-match): deprecation rules are configuration overrides, not guards, so a V1 base controller’s catch-all ‘deprecate_actions` is naturally overridden by a later, action-specific declaration in a subclass. Exactly one rule applies per request and exactly one Deprecation header is emitted. With no positional actions a rule is a catch-all for the whole controller (the WebhookVerifiable convention).
‘after_sunset: :gone` (requires `sunset_at`) halts with 410 once the sunset instant is reached (the boundary instant counts as sunset — inclusive). The default `:headers` NEVER blocks, however long past sunset — flip to `:gone` only once metrics show callers have migrated. `on_deprecated_access` is that metrics seam: it instruments “deprecated_endpoint.concerns_on_rails” and runs `notify:`. A raising `notify` propagates on purpose — a broken metrics hook should be loud, not silently swallowed (WebhookVerifiable’s stance).
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- LABEL =
"ConcernsOnRails::Controllers::Deprecatable".freeze
- VALID_AFTER_SUNSET =
%i[headers gone].freeze
- VALID_HEADER_FORMATS =
%i[rfc9745 legacy].freeze
- UNPARSEABLE =
"could not be parsed (pass a Time, Date, DateTime, or parseable String)".freeze
Instance Method Summary collapse
-
#apply_api_deprecations ⇒ Object
before_action entry point.
-
#deprecation_active? ⇒ Boolean
True when some rule covers the current action.
-
#on_deprecated_access(rule) ⇒ Object
Public override point + instrumentation seam.
-
#sunset_passed? ⇒ Boolean
True when the matching rule has a sunset_at that the clock has reached.
Instance Method Details
#apply_api_deprecations ⇒ Object
before_action entry point. Public so host apps can ‘skip_before_action :apply_api_deprecations` or override it.
159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/concerns_on_rails/controllers/deprecatable.rb', line 159 def apply_api_deprecations rule = deprecation_rule_for_action return nil unless rule # Order matters: headers go out unconditionally (so even the 410 carries # them), THEN we record the access, THEN enforce. Recording before # enforcing means metrics still count callers who get the 410. emit_deprecation_headers(rule) on_deprecated_access(rule) enforce_deprecation_sunset(rule) end |
#deprecation_active? ⇒ Boolean
True when some rule covers the current action.
185 186 187 |
# File 'lib/concerns_on_rails/controllers/deprecatable.rb', line 185 def deprecation_active? !deprecation_rule_for_action.nil? end |
#on_deprecated_access(rule) ⇒ Object
Public override point + instrumentation seam. Default: emit an ActiveSupport::Notifications event and run the rule’s ‘notify:` callable (instance_exec’d, so it can read controller state). A raising ‘notify` propagates by design.
175 176 177 178 179 180 181 182 |
# File 'lib/concerns_on_rails/controllers/deprecatable.rb', line 175 def on_deprecated_access(rule) ActiveSupport::Notifications.instrument( "deprecated_endpoint.concerns_on_rails", controller: deprecation_controller_name, action: deprecation_action_name, deprecated_at: rule[:deprecated_at], sunset_at: rule[:sunset_at] ) instance_exec(&rule[:notify]) if rule[:notify] end |
#sunset_passed? ⇒ Boolean
True when the matching rule has a sunset_at that the clock has reached.
190 191 192 |
# File 'lib/concerns_on_rails/controllers/deprecatable.rb', line 190 def sunset_passed? deprecation_sunset_reached?(deprecation_rule_for_action) end |