diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1fe04c1..9e83bdeed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * [#2786](https://github.com/ruby-grape/grape/pull/2786): Make route `requirements` and `anchor` explicit keyword arguments and first-class endpoint inputs instead of opaque `route_options` keys - [@ericproulx](https://github.com/ericproulx). * [#2788](https://github.com/ruby-grape/grape/pull/2788): Compare the base API instead of `to_s` when refreshing a mounted app - [@ericproulx](https://github.com/ericproulx). * [#2796](https://github.com/ruby-grape/grape/pull/2796): Remove `Grape::Util::ReverseStackableValues`, storing rescue handlers in plain per-scope hashes merged over `InheritableSetting#parent` - [@ericproulx](https://github.com/ericproulx). +* [#2806](https://github.com/ruby-grape/grape/pull/2806): Internalize versioning state behind `Grape::Util::InheritableSetting` accessors (`version`, `version_options`, `root_prefix`, `cascade` / `cascade_defined?`) - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index 9bd07d5df..8aad0fcd5 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -82,6 +82,10 @@ A route's declared params were previously carried inside the `route_options` bag Like `params` above, a route's `requirements` and `anchor` were previously carried inside the `route_options` bag. They are now composed into their own endpoint inputs (`Grape::Endpoint::Options` gains `:requirements` and `:anchor` members, and `Grape::Endpoint.new` gains `requirements:` and `anchor:` keywords) and exposed only through the `route.requirements` and `route.anchor` readers. `route.options[:requirements]` and `route.options[:anchor]` now return `nil` — including for a mount's `anchor: false`. Nothing in Grape or grape-swagger read them that way, so this only affects code that reached into the options Hash for these keys directly. +#### Versioning state is recorded through `InheritableSetting` accessors + +The nearest-wins scalars written by the `version`, `prefix` and `cascade` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`version` / `version=`, `version_options` / `version_options=`, `root_prefix` / `root_prefix=`, `cascade` / `cascade=`) instead of raw `namespace_inheritable` keys. `cascade` is presence-sensitive — an explicit `nil` is distinct from never-set — so the accessor family includes `cascade_defined?`, absorbing the `namespace_inheritable.key?(:cascade)` checks previously spelled out in `DSL::Routing#cascade` and `Grape::API::Instance#cascade?`. The keys' storage is unchanged for now, so `namespace_inheritable[:version]` and friends still return the same values, but they should be considered internal. + ### Upgrading to >= 3.3 #### Minimum required Ruby is now 3.3 diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index c9e39a3ff..49b874a2a 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -134,9 +134,9 @@ def call(env) # errors from reaching upstream. This is effectivelly done by unsetting # X-Cascade. Default :cascade is true. def cascade? - namespace_inheritable = self.class.inheritable_setting.namespace_inheritable - return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade) - return namespace_inheritable[:version_options].cascade if namespace_inheritable[:version_options] + setting = self.class.inheritable_setting + return setting.cascade if setting.cascade_defined? + return setting.version_options.cascade if setting.version_options true end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 20bbc7d07..dcc8964d3 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -16,9 +16,9 @@ def mounted(&block) end def cascade(value = nil) - return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable[:cascade].nil? : true if value.nil? + return inheritable_setting.cascade_defined? ? !inheritable_setting.cascade.nil? : true if value.nil? - inheritable_setting.namespace_inheritable[:cascade] = value + inheritable_setting.cascade = value end # Specify an API version. @@ -72,13 +72,13 @@ def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: fal if block within_namespace do - inheritable_setting.namespace_inheritable[:version] = requested_versions - inheritable_setting.namespace_inheritable[:version_options] = options + inheritable_setting.version = requested_versions + inheritable_setting.version_options = options instance_eval(&block) end else - inheritable_setting.namespace_inheritable[:version] = requested_versions - inheritable_setting.namespace_inheritable[:version_options] = options + inheritable_setting.version = requested_versions + inheritable_setting.version_options = options end @versions&.last @@ -86,9 +86,9 @@ def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: fal # Define a root URL prefix for your entire API. def prefix(prefix = nil) - return inheritable_setting.namespace_inheritable[:root_prefix] if prefix.nil? + return inheritable_setting.root_prefix if prefix.nil? - inheritable_setting.namespace_inheritable[:root_prefix] = prefix.to_s + inheritable_setting.root_prefix = prefix.to_s end # Create a scope without affecting the URL. diff --git a/lib/grape/dsl/version_options.rb b/lib/grape/dsl/version_options.rb index 0cd6c6598..908f78ffe 100644 --- a/lib/grape/dsl/version_options.rb +++ b/lib/grape/dsl/version_options.rb @@ -4,7 +4,7 @@ module Grape module DSL # Immutable value object holding the resolved options from # +Grape::DSL::Routing#version+. Stored on the inheritable settings as - # +namespace_inheritable[:version_options]+ and read by internal call + # +Grape::Util::InheritableSetting#version_options+ and read by internal call # sites (`Path`, `Endpoint`, `API::Instance#cascade?`, # `Middleware::Versioner::Base`) via accessors. # diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 9502cdc2c..f54cbf976 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -292,8 +292,8 @@ def to_routes params = config.params path_settings = prepare_default_path_settings forward_match = bare_rack_app? - version = prepare_version(inheritable_setting.namespace_inheritable[:version]) - prefix = inheritable_setting.namespace_inheritable[:root_prefix] + version = prepare_version(inheritable_setting.version) + prefix = inheritable_setting.root_prefix requirements = prepare_routes_requirements(config.requirements) anchor = config.anchor settings = inheritable_setting.route.except(:declared_params, :saved_validations) @@ -351,12 +351,12 @@ def build_stack stack.concat inheritable_setting.namespace_stackable[:middleware] - if inheritable_setting.namespace_inheritable[:version].present? - version_options = inheritable_setting.namespace_inheritable[:version_options] + if inheritable_setting.version.present? + version_options = inheritable_setting.version_options stack.use Grape::Middleware::Versioner.using(version_options.using), - versions: inheritable_setting.namespace_inheritable[:version].flatten, + versions: inheritable_setting.version.flatten, version_options:, - prefix: inheritable_setting.namespace_inheritable[:root_prefix], + prefix: inheritable_setting.root_prefix, mount_path: inheritable_setting.namespace_stackable[:mount_path].first end diff --git a/lib/grape/util/inheritable_setting.rb b/lib/grape/util/inheritable_setting.rb index 912b1f68d..7cb6e8731 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -106,6 +106,53 @@ def namespace_stackable_with_hash(key) data.each_with_object({}) { |value, result| result.deep_merge!(value) } end + # Versioning state recorded by the routing DSL (see DSL::Routing): + # +version+ holds the Array of version strings registered by the + # +version+ DSL method, +version_options+ its DSL::VersionOptions + # value object, and +root_prefix+ the path prefix set by +prefix+. + # Nearest-wins scalars with plain += writers; readers return nil when + # never set; the backing store is an internal detail. + def version + namespace_inheritable[:version] + end + + def version=(versions) + namespace_inheritable[:version] = versions + end + + def version_options + namespace_inheritable[:version_options] + end + + def version_options=(options) + namespace_inheritable[:version_options] = options + end + + def root_prefix + namespace_inheritable[:root_prefix] + end + + def root_prefix=(prefix) + namespace_inheritable[:root_prefix] = prefix + end + + # Cascade flag assigned by the +cascade+ DSL. An explicit nil is + # meaningful and distinct from never-set (the backing store is + # key-presence based), so #cascade_defined? reports whether any scope + # assigned it — Grape::API::Instance#cascade? falls back to the + # version options' cascade, then to true, when it was never assigned. + def cascade + namespace_inheritable[:cascade] + end + + def cascade=(value) + namespace_inheritable[:cascade] = value + end + + def cascade_defined? + namespace_inheritable.key?(:cascade) + end + # Rescue-handler maps registered by +rescue_from+, keyed by exception # class and merged so a nested scope's handler wins. Record them with # #add_rescue_handlers; the backing store is an internal detail. diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 57afe0c47..262c69334 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -24,8 +24,8 @@ class << self it 'sets a version for route' do version = 'v1' expect(subject.version(version)).to eq(version) - expect(subject.inheritable_setting.namespace_inheritable[:version]).to eq([version]) - expect(subject.inheritable_setting.namespace_inheritable[:version_options]).to eq(Grape::DSL::VersionOptions.new) + expect(subject.inheritable_setting.version).to eq([version]) + expect(subject.inheritable_setting.version_options).to eq(Grape::DSL::VersionOptions.new) end end @@ -33,7 +33,7 @@ class << self it 'sets a prefix for route' do prefix = '/api' subject.prefix prefix - expect(subject.inheritable_setting.namespace_inheritable[:root_prefix]).to eq(prefix) + expect(subject.inheritable_setting.root_prefix).to eq(prefix) end end