diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1fe04c1..8d97cfdab 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). +* [#2803](https://github.com/ruby-grape/grape/pull/2803): Internalize namespace and mount-path registrations behind `Grape::Util::InheritableSetting` accessors (`namespaces` / `add_namespace` / `namespace_path` / `namespace_requirements`, `mount_path` / `add_mount_path`) - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index 9bd07d5df..ec22e11f0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -82,6 +82,16 @@ 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. +#### Namespace and mount-path registrations are recorded through `InheritableSetting` accessors + +The `Grape::Namespace` objects registered by the `namespace` DSL (and its `group` / `resource` / `resources` / `segment` aliases) and the mount path recorded by `mount` are now written and read through dedicated accessors on `Grape::Util::InheritableSetting` instead of raw `namespace_stackable` keys, following the same move made for rescue handlers: + +* `namespaces` / `add_namespace(namespace)` replace `namespace_stackable[:namespace]` (not to be confused with the pre-existing `namespace` reader, which returns the `InheritableValues` store). +* `namespace_path` returns the normalized joined path prefix, absorbing the `Grape::Namespace.joined_space_path(...)` call previously spelled out at the read sites, and `namespace_requirements` returns the requirements declared by registered namespaces, absorbing the `filter_map(&:requirements)`. +* `mount_path` / `add_mount_path(path)` replace `namespace_stackable[:mount_path]`; the reader absorbs the outermost-wins convention (previously the `.first` at the read site in `Endpoint#build_stack`). + +The keys' storage is unchanged for now, so `namespace_stackable[:namespace]` and `namespace_stackable[:mount_path]` 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/dsl/routing.rb b/lib/grape/dsl/routing.rb index 20bbc7d07..9fbc519e6 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -137,7 +137,7 @@ def mount(mounts, *opts) # instantiated Grape::API::Instance (vs. a bare Rack app). if app.is_a?(Grape::Mountable) mount_path = Grape::Util::PathNormalizer.call(path) - app.top_level_setting.namespace_stackable[:mount_path] = mount_path + app.top_level_setting.add_mount_path(mount_path) app.inherit_settings(inheritable_setting) @@ -229,11 +229,11 @@ def route(methods, paths = ['/'], requirements: nil, anchor: true, **route_optio # end # end def namespace(space = nil, requirements: nil, **options, &block) - return Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) unless space || block + return inheritable_setting.namespace_path unless space || block within_namespace do nest(block) do - inheritable_setting.namespace_stackable[:namespace] = Grape::Namespace.new(space, requirements:, **options) if space + inheritable_setting.add_namespace(Grape::Namespace.new(space, requirements:, **options)) if space end end end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 9502cdc2c..062d9d965 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -131,7 +131,7 @@ def mount_in(router) end def namespace - @namespace ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) + @namespace ||= inheritable_setting.namespace_path end def call(env) @@ -328,7 +328,7 @@ def prepare_default_path_settings end def prepare_routes_requirements(route_options_requirements) - namespace_requirements = inheritable_setting.namespace_stackable[:namespace].filter_map(&:requirements) + namespace_requirements = inheritable_setting.namespace_requirements namespace_requirements << route_options_requirements if route_options_requirements.present? namespace_requirements.reduce({}, :merge) end @@ -357,7 +357,7 @@ def build_stack versions: inheritable_setting.namespace_inheritable[:version].flatten, version_options:, prefix: inheritable_setting.namespace_inheritable[:root_prefix], - mount_path: inheritable_setting.namespace_stackable[:mount_path].first + mount_path: inheritable_setting.mount_path end stack.use Grape::Middleware::Formatter, diff --git a/lib/grape/namespace.rb b/lib/grape/namespace.rb index ffff81de1..3d467ed4b 100644 --- a/lib/grape/namespace.rb +++ b/lib/grape/namespace.rb @@ -35,8 +35,9 @@ def hash [self.class, space, requirements, options].hash end - # Join the namespaces from a list of settings to create a path prefix. - # @param settings [Array] list of Grape::Util::InheritableSettings. + # Join the namespaces from a list of Namespace objects to create a path + # prefix. + # @param settings [Array] list of Grape::Namespace objects. def self.joined_space_path(settings) JoinedSpaceCache[joined_space(settings)] end diff --git a/lib/grape/util/inheritable_setting.rb b/lib/grape/util/inheritable_setting.rb index 912b1f68d..5e4976219 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -106,6 +106,43 @@ def namespace_stackable_with_hash(key) data.each_with_object({}) { |value, result| result.deep_merge!(value) } end + # Grape::Namespace objects registered by the +namespace+ DSL and its + # aliases (group, resource, resources, segment; see DSL::Routing), + # outermost scope first. Not to be confused with the #namespace values + # store. Record them with #add_namespace; the backing store is an + # internal detail. + def namespaces + namespace_stackable[:namespace] + end + + def add_namespace(namespace) + namespace_stackable[:namespace] = namespace + end + + # The normalized path prefix formed by joining every registered + # namespace's space (see Grape::Namespace.joined_space_path). + def namespace_path + Grape::Namespace.joined_space_path(namespaces) + end + + # The param requirements declared by registered namespaces, outermost + # scope first. + def namespace_requirements + namespaces.filter_map(&:requirements) + end + + # The path a Grape API is mounted under, recorded on the mounted API's + # top-level settings by +mount+ (see DSL::Routing). Reading returns the + # outermost mount path — nil when the API is not mounted; the backing + # store is an internal detail. + def mount_path + namespace_stackable[:mount_path].first + end + + def add_mount_path(mount_path) + namespace_stackable[:mount_path] = mount_path + 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.