diff --git a/.rubocop.yml b/.rubocop.yml index 3ece55d1f..114b2cab7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -52,7 +52,7 @@ Metrics/BlockLength: - spec/**/*_spec.rb Metrics/ClassLength: - Max: 305 + Max: 320 Metrics/CyclomaticComplexity: Max: 15 diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1fe04c1..8677c4562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,19 @@ * [#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). +* [#2797](https://github.com/ruby-grape/grape/pull/2797): Internalize params state (validations, declared params, param documentation, named params) behind `Grape::Util::InheritableSetting` accessors, and remove the unused `api_class` / `point_in_time_copies` readers - [@ericproulx](https://github.com/ericproulx). +* [#2798](https://github.com/ruby-grape/grape/pull/2798): Internalize callback filters behind `Grape::Util::InheritableSetting` accessors (`callbacks` / `add_callback`) - [@ericproulx](https://github.com/ericproulx). +* [#2799](https://github.com/ruby-grape/grape/pull/2799): Internalize the remaining rescue configuration (`rescue_options`, meta-selector handlers and flags) behind `Grape::Util::InheritableSetting` accessors - [@ericproulx](https://github.com/ericproulx). +* [#2800](https://github.com/ruby-grape/grape/pull/2800): Internalize content negotiation state (content types, formatters, parsers, error formatters) behind `Grape::Util::InheritableSetting` accessors - [@ericproulx](https://github.com/ericproulx). +* [#2801](https://github.com/ruby-grape/grape/pull/2801): Internalize `represent` registrations behind `Grape::Util::InheritableSetting` accessors (`representations` / `add_representation`), and remove the inert `:representations` seeding in `Grape::Endpoint#initialize` - [@ericproulx](https://github.com/ericproulx). +* [#2802](https://github.com/ruby-grape/grape/pull/2802): Internalize middleware and helper registrations behind `Grape::Util::InheritableSetting` accessors (`middleware` / `add_middleware`, `helpers` / `add_helper`) - [@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). +* [#2804](https://github.com/ruby-grape/grape/pull/2804): Internalize contract key maps behind `Grape::Util::InheritableSetting` accessors (`contract_key_maps` / `add_contract_key_map`) - [@ericproulx](https://github.com/ericproulx). +* [#2805](https://github.com/ruby-grape/grape/pull/2805): Internalize format and error-response defaults behind `Grape::Util::InheritableSetting` accessors (`format`, `default_format`, `default_error_formatter`, `default_error_status`) - [@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). +* [#2807](https://github.com/ruby-grape/grape/pull/2807): Internalize routing scope flags behind `Grape::Util::InheritableSetting` accessors (`do_not_route_head`, `do_not_route_options`, `do_not_document`, `lint` — `!` writers / `?` readers) - [@ericproulx](https://github.com/ericproulx). +* [#2808](https://github.com/ruby-grape/grape/pull/2808): Internalize `build_params_with` and `auth` behind `Grape::Util::InheritableSetting` accessors - [@ericproulx](https://github.com/ericproulx). +* [#2807](https://github.com/ruby-grape/grape/pull/2807): Internalize routing scope flags behind `Grape::Util::InheritableSetting` accessors (`do_not_route_head`, `do_not_route_options`, `do_not_document`, `lint` — `!` writers / `?` readers), and drop the vestigial `namespace_inheritable` hash copy in `Grape::API::Instance`'s route-config collection - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/UPGRADING.md b/UPGRADING.md index 9bd07d5df..926d3e657 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -82,6 +82,65 @@ 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. +#### Params state is recorded through `InheritableSetting` accessors + +The state accumulated by `params` / `contract` blocks — validator instances, declared params, param documentation and reusable named params — is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`validations` / `add_validation`, `declared_params` / `add_declared_params`, `params_documentation` / `add_params_documentation`, `named_params` / `add_named_params`, `reset_validations!`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:validations]` and friends still return the same values, but they should be considered internal. + +Related contract changes, none with known external consumers: + +* `Grape::Endpoint#inherit_settings` now takes the parent `Grape::Util::InheritableSetting` instead of that setting's raw `namespace_stackable` store. +* The endpoint's request-time validator snapshot moved from `route_setting(:saved_validations)` to `route_setting(:validations)`, symmetric with `route_setting(:declared_params)`. The vestigial `saved_` prefix dated back to the 2014 settings refactor. Neither key appears in `route.settings`, as before. +* `Grape::Util::InheritableSetting#api_class` (a Hash nothing in Grape ever wrote to) and `#point_in_time_copies` (only ever used internally) are removed. +#### Callback filters are recorded through `InheritableSetting` accessors + +The filter blocks registered by `before`, `before_validation`, `after_validation`, `after` and `finally` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` — `add_callback(name, block)` to record, and `callbacks` returning a Hash keyed by the DSL method names (`callbacks[:before]`, `callbacks[:finally]`, …) — instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:befores]` and friends still return the same values, but the pluralized keys should be considered internal. +#### Rescue configuration is recorded through `InheritableSetting` accessors + +The remaining rescue state written by `rescue_from` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting`, completing the encapsulation started with `rescue_handlers` / `add_rescue_handlers`: + +* `rescue_options` / `add_rescue_options(options)` replace `namespace_stackable[:rescue_options]`. The reader absorbs the nearest-scope-wins convention (previously the `&.last` at the read site) and returns a single `Grape::DSL::RescueOptions` or `nil`, not the raw stack. +* `add_all_rescue_handler(handler)`, `add_grape_exceptions_rescue_handler(handler)` and `add_internal_grape_exceptions_rescue_handler(handler)` replace the direct `namespace_inheritable` writes for the `rescue_from :all` / `:grape_exceptions` / `:internal_grape_exceptions` meta selectors; each records its handler and flips the associated flags. +* `rescue_all?`, `rescue_grape_exceptions?`, `all_rescue_handler`, `grape_exceptions_rescue_handler` and `internal_grape_exceptions_rescue_handler` replace the corresponding `namespace_inheritable` reads. The two `?` readers return `false` (rather than `nil`) when never set; `Grape::Middleware::Error` only ever used them in boolean context, so behavior is unchanged. + +The keys' storage is unchanged for now, so `namespace_stackable[:rescue_options]` and the `namespace_inheritable` keys still return the same values, but they should be considered internal. +#### Content negotiation state is recorded through `InheritableSetting` accessors + +The state written by the `content_type`, `format`, `formatter`, `parser` and `error_formatter` DSL methods is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`content_types` / `add_content_type`, `formatters` / `add_formatter`, `parsers` / `add_parser`, `error_formatters` / `add_error_formatter`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. Each writer records one single-entry Hash per registration and each reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged name => value Hash (nearest scope wins) or `nil` when nothing is registered. The keys' storage is unchanged for now, so `namespace_stackable[:content_types]` and friends still return the same values, but they should be considered internal. +#### `represent` registrations are recorded through `InheritableSetting` accessors + +The model-class => entity-class registrations written by `represent` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`representations` / `add_representation(model_class, entity_class)`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged Hash (nearest scope wins) or `nil` when nothing is registered. The key's storage is unchanged for now, so `namespace_stackable[:representations]` still returns the same values, but it should be considered internal. + +Also removed: the `namespace_stackable[:representations] ||= []` line in `Grape::Endpoint#initialize`. It was provably inert — `Grape::Util::StackableValues#[]` always returns an array (a frozen empty one for never-written keys), which is truthy, so the `||=` assignment could never execute. No settings state changes as a result. +#### Middleware and helper registrations are recorded through `InheritableSetting` accessors + +The state written by the middleware DSL (`use`, `insert`, `insert_before`, `insert_after`) and by `helpers` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`middleware` / `add_middleware`, `helpers` / `add_helper`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:middleware]` and `namespace_stackable[:helpers]` still return the same values, but they should be considered internal. + +One micro-change: the `middleware` DSL reader dropped its `|| []` fallback — `Grape::Util::StackableValues#[]` never returns `nil`, so the fallback was dead code. When no middleware is registered the reader now returns the shared frozen empty array instead of a fresh mutable one; no caller in Grape or grape-swagger mutates the returned array. +#### 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. +#### Contract key maps are recorded through `InheritableSetting` accessors + +The Dry::Schema key maps registered by `contract` blocks are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`contract_key_maps` / `add_contract_key_map(key_map)`) instead of the raw `namespace_stackable[:contract_key_map]` key, following the same move made for rescue handlers. The key's storage is unchanged for now, so `namespace_stackable[:contract_key_map]` still returns the same values, but it should be considered internal. +#### Format and error-response defaults are recorded through `InheritableSetting` accessors + +The nearest-wins scalars written by the `format`, `default_format`, `default_error_formatter` and `default_error_status` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`format` / `format=`, `default_format` / `default_format=`, `default_error_formatter` / `default_error_formatter=`, `default_error_status` / `default_error_status=`) instead of raw `namespace_inheritable` keys — the first batch of the `namespace_inheritable` cleanup, following the pattern used for `namespace_stackable`. Since these are overriding assignments rather than stacking registrations, the writers use plain `=` instead of the `add_*` naming. The keys' storage is unchanged for now, so `namespace_inheritable[:format]` and friends still return the same values, but they should be considered internal. +#### 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. +#### Routing scope flags are recorded through `InheritableSetting` accessors + +The flags flipped by `do_not_route_head!`, `do_not_route_options!`, `do_not_document!` and `lint!` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`!` writers, `?` readers) instead of raw `namespace_inheritable` keys. The `?` readers return `false` (rather than `nil`) when never set; every consumer only used them in boolean context, so behavior is unchanged. The keys' storage is unchanged for now, so `namespace_inheritable[:do_not_route_head]` and friends still return the same values, but they should be considered internal. +#### `build_params_with` and `auth` are recorded through `InheritableSetting` accessors + +The params-builder strategy written by `build_with` (both the API-level and the params-block DSL) and the authentication configuration written by the `auth` DSL are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`build_params_with` / `build_params_with=`, `auth` / `auth=`) instead of raw `namespace_inheritable` keys, completing the per-key `namespace_inheritable` cleanup. The keys' storage is unchanged for now, so `namespace_inheritable[:build_params_with]` and `namespace_inheritable[:auth]` 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..1b7104d54 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -83,7 +83,7 @@ def inherit_settings(other_settings) # Propagate any inherited params down to our endpoints, and reset any # compiled routes. endpoints.each do |e| - e.inherit_settings(top_level_setting.namespace_stackable) + e.inherit_settings(top_level_setting) e.reset_routes! end @@ -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 @@ -154,18 +154,11 @@ def add_head_not_allowed_methods_and_options_methods # contain already versioning information when using path versioning. all_routes = self.class.endpoints.flat_map(&:routes) - # Read the settings the helper routes need from a *copy* with the - # root-prefix/versioning keys stripped, so adding these routes won't - # prepend versioning information again. This used to delete those keys - # from the shared class-level settings and restore them in an ensure; - # a request served concurrently on another instance during a runtime - # recompile could observe the missing keys (e.g. via #cascade?). A - # local copy keeps that mutation off the shared object. - namespace_inheritable = self.class.inheritable_setting.namespace_inheritable.to_hash.except(*ROOT_PREFIX_VERSIONING_KEYS) - collect_route_config_per_pattern(all_routes, namespace_inheritable) + collect_route_config_per_pattern(all_routes) end - def collect_route_config_per_pattern(all_routes, namespace_inheritable) + def collect_route_config_per_pattern(all_routes) + setting = self.class.inheritable_setting routes_by_regexp = all_routes.group_by(&:pattern_regexp) # Build the configuration based on the first endpoint and the collection of methods supported. @@ -174,18 +167,15 @@ def collect_route_config_per_pattern(all_routes, namespace_inheritable) last_route = routes.last # Most of the configuration is taken from the last endpoint allowed_methods = routes.map(&:request_method) - allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET) + allowed_methods |= [Rack::HEAD] if !setting.do_not_route_head? && allowed_methods.include?(Rack::GET) - allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods - last_route.app.options_route_enabled = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS) + allow_header = setting.do_not_route_options? ? allowed_methods : [Rack::OPTIONS] | allowed_methods + last_route.app.options_route_enabled = true unless setting.do_not_route_options? || allowed_methods.include?(Rack::OPTIONS) greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:) @router.associate_routes(greedy_route) end end - - ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze - private_constant :ROOT_PREFIX_VERSIONING_KEYS end end end diff --git a/lib/grape/dsl/callbacks.rb b/lib/grape/dsl/callbacks.rb index beb0e5818..daf6f87f1 100644 --- a/lib/grape/dsl/callbacks.rb +++ b/lib/grape/dsl/callbacks.rb @@ -9,15 +9,9 @@ module Callbacks # after: execute the given block after the endpoint code has run except in unsuccessful # finally: execute the given block after the endpoint code even if unsuccessful - { - before: :befores, - before_validation: :before_validations, - after_validation: :after_validations, - after: :afters, - finally: :finallies - }.each do |method_name, plural_key| - define_method method_name do |&block| - inheritable_setting.namespace_stackable[plural_key] = block + %i[before before_validation after_validation after finally].each do |callback_name| + define_method callback_name do |&block| + inheritable_setting.add_callback(callback_name, block) end end end diff --git a/lib/grape/dsl/declared.rb b/lib/grape/dsl/declared.rb index 907dcf90f..05096bd6b 100644 --- a/lib/grape/dsl/declared.rb +++ b/lib/grape/dsl/declared.rb @@ -22,9 +22,9 @@ def initialize(msg = '#declared is not available prior to parameter validation') def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false) raise MethodNotYetAvailable unless before_filter_passed - contract_key_map = inheritable_setting.namespace_stackable[:contract_key_map] + contract_key_map = inheritable_setting.contract_key_maps handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:) - declared_params = include_parent_namespaces ? inheritable_setting.route[:declared_params] : (inheritable_setting.namespace_stackable[:declared_params].last || []) + declared_params = include_parent_namespaces ? inheritable_setting.route[:declared_params] : (inheritable_setting.declared_params.last || []) renamed_params = inheritable_setting.route[:renamed_params] || {} route_params = config.params diff --git a/lib/grape/dsl/entity.rb b/lib/grape/dsl/entity.rb index 5f1ff9c7c..964c43407 100644 --- a/lib/grape/dsl/entity.rb +++ b/lib/grape/dsl/entity.rb @@ -51,7 +51,7 @@ def present(*args, root: nil, with: nil, **options) def entity_class_for_obj(object) klass = object_class(object) - representations = inheritable_setting.namespace_stackable_with_hash(:representations) + representations = inheritable_setting.representations if representations potential = klass.ancestors.detect { |potential| representations.key?(potential) } return representations[potential] if potential && representations[potential] diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index 03d7a524e..20769cf55 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -52,12 +52,12 @@ def include_block(block) def make_inclusion(mod, &) define_boolean_in_mod(mod) inject_api_helpers_to_mod(mod, &) - inheritable_setting.namespace_stackable[:helpers] = mod + inheritable_setting.add_helper(mod) end def include_all_in_scope mod = Module.new - namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include } + inheritable_setting.helpers.each { |mod_to_include| mod.include mod_to_include } change! mod end @@ -115,7 +115,7 @@ def api_changed(new_api) def process_named_params return if @named_params.blank? - api.inheritable_setting.namespace_stackable[:named_params] = @named_params + api.inheritable_setting.add_named_params(@named_params) end end end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 2a68de77e..2a48fed36 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -27,7 +27,7 @@ def configuration # @param backtrace [Array] The backtrace of the exception that caused the error. # @param original_exception [Exception] The original exception that caused the error. def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil) - status = self.status(status || inheritable_setting.namespace_inheritable[:default_error_status]) + status = self.status(status || inheritable_setting.default_error_status) headers = additional_headers.present? ? header.merge(additional_headers) : header throw :error, Grape::Exceptions::ErrorResponse.new( message:, status:, headers:, backtrace:, original_exception: diff --git a/lib/grape/dsl/middleware.rb b/lib/grape/dsl/middleware.rb index 20add8a9f..18ec5e1be 100644 --- a/lib/grape/dsl/middleware.rb +++ b/lib/grape/dsl/middleware.rb @@ -13,7 +13,7 @@ def use(middleware_class, *args, &block) arr = [:use, middleware_class, *args] arr << block if block - inheritable_setting.namespace_stackable[:middleware] = arr + inheritable_setting.add_middleware(arr) end %i[insert insert_before insert_after].each do |method_name| @@ -21,7 +21,7 @@ def use(middleware_class, *args, &block) arr = [method_name, *args] arr << block if block - inheritable_setting.namespace_stackable[:middleware] = arr + inheritable_setting.add_middleware(arr) end end @@ -29,7 +29,7 @@ def use(middleware_class, *args, &block) # and arguments that are currently applied to the # application. def middleware - inheritable_setting.namespace_stackable[:middleware] || [] + inheritable_setting.middleware end end end diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index babb10d18..001c4283e 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -29,7 +29,7 @@ module Parameters # end # end def build_with(build_with) - @api.inheritable_setting.namespace_inheritable[:build_params_with] = build_with + @api.inheritable_setting.build_params_with = build_with end # Include reusable params rules among current. @@ -54,7 +54,7 @@ def build_with(build_with) # end # end def use(*names, **options) - named_params = @api.inheritable_setting.namespace_stackable_with_hash(:named_params) || {} + named_params = @api.inheritable_setting.named_params || {} names.each do |name| params_block = named_params.fetch(name) do raise "Params :#{name} not found!" diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index c942c2656..5f94be98f 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -6,66 +6,64 @@ module RequestResponse # Specify the default format for the API's serializers. # May be `:json` or `:txt` (default). def default_format(new_format = nil) - return inheritable_setting.namespace_inheritable[:default_format] if new_format.nil? + return inheritable_setting.default_format if new_format.nil? - inheritable_setting.namespace_inheritable[:default_format] = new_format.to_sym + inheritable_setting.default_format = new_format.to_sym end # Specify the format for the API's serializers. # May be `:json`, `:xml`, `:txt`, etc. def format(new_format = nil) - return inheritable_setting.namespace_inheritable[:format] if new_format.nil? + return inheritable_setting.format if new_format.nil? symbolic_new_format = new_format.to_sym - inheritable_setting.namespace_inheritable[:format] = symbolic_new_format - inheritable_setting.namespace_inheritable[:default_error_formatter] = Grape::ErrorFormatter.formatter_for(symbolic_new_format) + inheritable_setting.format = symbolic_new_format + inheritable_setting.default_error_formatter = Grape::ErrorFormatter.formatter_for(symbolic_new_format) content_type = content_types[symbolic_new_format] raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type - inheritable_setting.namespace_stackable[:content_types] = { symbolic_new_format => content_type } + inheritable_setting.add_content_type(symbolic_new_format, content_type) end # Specify a custom formatter for a content-type. def formatter(content_type, new_formatter) - inheritable_setting.namespace_stackable[:formatters] = { content_type.to_sym => new_formatter } + inheritable_setting.add_formatter(content_type.to_sym, new_formatter) end # Specify a custom parser for a content-type. def parser(content_type, new_parser) - inheritable_setting.namespace_stackable[:parsers] = { content_type.to_sym => new_parser } + inheritable_setting.add_parser(content_type.to_sym, new_parser) end # Specify a default error formatter. def default_error_formatter(new_formatter_name = nil) - return inheritable_setting.namespace_inheritable[:default_error_formatter] if new_formatter_name.nil? + return inheritable_setting.default_error_formatter if new_formatter_name.nil? - new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) - inheritable_setting.namespace_inheritable[:default_error_formatter] = new_formatter + inheritable_setting.default_error_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) end def error_formatter(format, options = nil, with: nil) formatter = with || options - inheritable_setting.namespace_stackable[:error_formatters] = { format.to_sym => formatter } + inheritable_setting.add_error_formatter(format.to_sym, formatter) end # Specify additional content-types, e.g.: # content_type :xls, 'application/vnd.ms-excel' def content_type(key, val) - inheritable_setting.namespace_stackable[:content_types] = { key.to_sym => val } + inheritable_setting.add_content_type(key.to_sym, val) end # All available content types. def content_types - c_types = inheritable_setting.namespace_stackable_with_hash(:content_types) - Grape::ContentTypes.content_types_for c_types + Grape::ContentTypes.content_types_for(inheritable_setting.content_types) end # Specify the default status code for errors. def default_error_status(new_status = nil) - return inheritable_setting.namespace_inheritable[:default_error_status] if new_status.nil? + return inheritable_setting.default_error_status if new_status.nil? - inheritable_setting.namespace_inheritable[:default_error_status] = new_status + inheritable_setting.default_error_status = new_status end # Allows you to rescue certain exceptions that occur to return @@ -101,29 +99,18 @@ def rescue_from(*args, with: nil, rescue_subclasses: true, backtrace: false, ori meta_selector = (args & META_RESCUE_SELECTORS).first raise ArgumentError, "rescue_from #{meta_selector.inspect} does not accept additional arguments" if meta_selector && args.size > 1 - namespace_inheritable = nil - arg = nil - - if args.one? - arg = args.first - namespace_inheritable = inheritable_setting.namespace_inheritable - end - - case arg + case meta_selector when :all - namespace_inheritable[:rescue_all] = true - namespace_inheritable[:all_rescue_handler] = handler + inheritable_setting.add_all_rescue_handler(handler) when :grape_exceptions - namespace_inheritable[:rescue_all] = true - namespace_inheritable[:rescue_grape_exceptions] = true - namespace_inheritable[:grape_exceptions_rescue_handler] = handler + inheritable_setting.add_grape_exceptions_rescue_handler(handler) when :internal_grape_exceptions - namespace_inheritable[:internal_grape_exceptions_rescue_handler] = handler + inheritable_setting.add_internal_grape_exceptions_rescue_handler(handler) else inheritable_setting.add_rescue_handlers(args.to_h { |klass| [klass, handler] }, subclasses: rescue_subclasses) end - inheritable_setting.namespace_stackable[:rescue_options] = RescueOptions.new(backtrace:, original_exception:) + inheritable_setting.add_rescue_options(RescueOptions.new(backtrace:, original_exception:)) end # Allows you to specify a default representation entity for a @@ -149,7 +136,7 @@ def rescue_from(*args, with: nil, rescue_subclasses: true, backtrace: false, ori def represent(model_class, with:) raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless with.is_a?(Class) - inheritable_setting.namespace_stackable[:representations] = { model_class => with } + inheritable_setting.add_representation(model_class, with) end private diff --git a/lib/grape/dsl/rescue_options.rb b/lib/grape/dsl/rescue_options.rb index f44a27569..fc9466289 100644 --- a/lib/grape/dsl/rescue_options.rb +++ b/lib/grape/dsl/rescue_options.rb @@ -3,9 +3,10 @@ module Grape module DSL # Immutable value object holding the response-shaping booleans accepted - # by +Grape::DSL::RequestResponse#rescue_from+. Stored on the - # inheritable settings as +namespace_stackable[:rescue_options]+ and - # delegated to by +Grape::Middleware::Error+ (which forwards + # by +Grape::DSL::RequestResponse#rescue_from+. Recorded on the + # inheritable settings via +Grape::Util::InheritableSetting#add_rescue_options+ + # (the nearest scope's latest registration wins on read, see + # +#rescue_options+) and delegated to by +Grape::Middleware::Error+ (which forwards # +backtrace+/+original_exception+ to the formatter as # +include_backtrace+/+include_original_exception+). # diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 20bbc7d07..c202959b3 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. @@ -102,25 +102,25 @@ def scope(_name = nil, &block) end def build_with(build_with) - inheritable_setting.namespace_inheritable[:build_params_with] = build_with + inheritable_setting.build_params_with = build_with end # Do not route HEAD requests to GET requests automatically. def do_not_route_head! - inheritable_setting.namespace_inheritable[:do_not_route_head] = true + inheritable_setting.do_not_route_head! end # Do not automatically route OPTIONS. def do_not_route_options! - inheritable_setting.namespace_inheritable[:do_not_route_options] = true + inheritable_setting.do_not_route_options! end def lint! - inheritable_setting.namespace_inheritable[:lint] = true + inheritable_setting.lint! end def do_not_document! - inheritable_setting.namespace_inheritable[:do_not_document] = true + inheritable_setting.do_not_document! end def mount(mounts, *opts) @@ -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 @@ -273,7 +273,7 @@ def versions # Compose a route's params: the declared params (+params do … end+) deep-merged # with any documented alongside +desc ..., params:+ (+description_params+). def prepare_params(description_params) - endpoint_params = inheritable_setting.namespace_stackable_with_hash(:params) || {} + endpoint_params = inheritable_setting.params_documentation || {} return endpoint_params if description_params.blank? endpoint_params.deep_merge(description_params) diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index 5ec5dc2b0..6c79b7e50 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -44,7 +44,7 @@ def contract(contract = nil, &block) # # whatever # end def reset_validations! - inheritable_setting.namespace_stackable.delete(:declared_params, :params, :validations) + inheritable_setting.reset_validations! end end end 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..d6388910a 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -67,16 +67,7 @@ def block_to_unbound_method(block) # endpoint does not know if it will be mounted under a different endpoint. # @yield a block defining what your API should do when this endpoint is hit def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block) - self.inheritable_setting = new_settings.point_in_time_copy - - # now +namespace_stackable(:declared_params)+ contains all params defined for - # this endpoint and its parents, but later it will be cleaned up, - # see +reset_validations!+ in lib/grape/dsl/validations.rb - inheritable_setting.route[:declared_params] = inheritable_setting.namespace_stackable[:declared_params].flatten - inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations].dup - - inheritable_setting.namespace_stackable[:representations] ||= [] - inheritable_setting.namespace_inheritable[:default_error_status] ||= 500 + self.inheritable_setting = new_settings.point_in_time_copy_for_endpoint @options = options @config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options) @@ -93,15 +84,16 @@ def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, r @endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints) end - # Update our settings from a given set of stackable parameters. Used when + # Update our settings from a given parent settings instance. Used when # the endpoint's API is mounted under another one. - def inherit_settings(namespace_stackable) - parent_validations = namespace_stackable[:validations] - inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any? - parent_declared_params = namespace_stackable[:declared_params] + # @param settings [Grape::Util::InheritableSetting] + def inherit_settings(settings) + parent_validations = settings.validations + inheritable_setting.route[:validations].concat(parent_validations) if parent_validations.any? + parent_declared_params = settings.declared_params inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any? - endpoints&.each { |e| e.inherit_settings(namespace_stackable) } + endpoints&.each { |e| e.inherit_settings(settings) } end def routes @@ -124,14 +116,14 @@ def mount_in(router) compile! routes.each do |route| router.append(route.apply(self)) - next if inheritable_setting.namespace_inheritable[:do_not_route_head] || route.request_method != Rack::GET + next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET router.append(route.to_head.apply(self)) end end def namespace - @namespace ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) + @namespace ||= inheritable_setting.namespace_path end def call(env) @@ -209,7 +201,7 @@ def execute end def run_validators(request:) - validators = inheritable_setting.route[:saved_validations] + validators = inheritable_setting.route[:validations] return if validators.blank? validation_exceptions = nil @@ -278,13 +270,13 @@ def compile! @app = config.app || build_stack warn_unauthenticated_mounted_app @helpers = build_helpers - stackable = inheritable_setting.namespace_stackable - @befores = stackable[:befores] - @before_validations = stackable[:before_validations] - @after_validations = stackable[:after_validations] - @afters = stackable[:afters] - @finallies = stackable[:finallies] - @build_params_with = inheritable_setting.namespace_inheritable[:build_params_with] + callbacks = inheritable_setting.callbacks + @befores = callbacks.fetch(:before) + @before_validations = callbacks.fetch(:before_validation) + @after_validations = callbacks.fetch(:after_validation) + @afters = callbacks.fetch(:after) + @finallies = callbacks.fetch(:finally) + @build_params_with = inheritable_setting.build_params_with end def to_routes @@ -292,11 +284,11 @@ 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) + settings = inheritable_setting.route.except(:declared_params, :validations) config.http_methods.flat_map do |method| config.path.map do |path| @@ -328,7 +320,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 @@ -342,30 +334,30 @@ def prepare_version(namespace_inheritable_version) def build_stack stack = Grape::Middleware::Stack.new - content_types = inheritable_setting.namespace_stackable_with_hash(:content_types) - format = inheritable_setting.namespace_inheritable[:format] + content_types = inheritable_setting.content_types + format = inheritable_setting.format stack.use Rack::Head stack.use Rack::Lint if lint? stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types) - stack.concat inheritable_setting.namespace_stackable[:middleware] + stack.concat inheritable_setting.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], - mount_path: inheritable_setting.namespace_stackable[:mount_path].first + prefix: inheritable_setting.root_prefix, + mount_path: inheritable_setting.mount_path end stack.use Grape::Middleware::Formatter, format:, - default_format: inheritable_setting.namespace_inheritable[:default_format] || :txt, + default_format: inheritable_setting.default_format || :txt, content_types:, - formatters: inheritable_setting.namespace_stackable_with_hash(:formatters), - parsers: inheritable_setting.namespace_stackable_with_hash(:parsers) + formatters: inheritable_setting.formatters, + parsers: inheritable_setting.parsers builder = stack.build builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } @@ -373,27 +365,26 @@ def build_stack end def error_middleware_options(format, content_types) - ns_inh = inheritable_setting.namespace_inheritable - ns_stack = inheritable_setting + setting = inheritable_setting { format:, content_types:, - default_status: ns_inh[:default_error_status], - rescue_all: ns_inh[:rescue_all], - rescue_grape_exceptions: ns_inh[:rescue_grape_exceptions], - default_error_formatter: ns_inh[:default_error_formatter], - error_formatters: ns_stack.namespace_stackable_with_hash(:error_formatters), - rescue_options: ns_stack.namespace_stackable[:rescue_options]&.last, - rescue_handlers: ns_stack.rescue_handlers, - base_only_rescue_handlers: ns_stack.base_only_rescue_handlers, - all_rescue_handler: ns_inh[:all_rescue_handler], - grape_exceptions_rescue_handler: ns_inh[:grape_exceptions_rescue_handler], - internal_grape_exceptions_rescue_handler: ns_inh[:internal_grape_exceptions_rescue_handler] + default_status: setting.default_error_status, + rescue_all: setting.rescue_all?, + rescue_grape_exceptions: setting.rescue_grape_exceptions?, + default_error_formatter: setting.default_error_formatter, + error_formatters: setting.error_formatters, + rescue_options: setting.rescue_options, + rescue_handlers: setting.rescue_handlers, + base_only_rescue_handlers: setting.base_only_rescue_handlers, + all_rescue_handler: setting.all_rescue_handler, + grape_exceptions_rescue_handler: setting.grape_exceptions_rescue_handler, + internal_grape_exceptions_rescue_handler: setting.internal_grape_exceptions_rescue_handler } end def build_helpers - helpers = inheritable_setting.namespace_stackable[:helpers] + helpers = inheritable_setting.helpers return if helpers.empty? Module.new { helpers.each { |mod_to_include| include mod_to_include } } @@ -406,7 +397,7 @@ def build_helpers # inherited settings. Warn so this bypass isn't silent. def warn_unauthenticated_mounted_app return unless bare_rack_app? - return unless inheritable_setting.namespace_inheritable[:auth] + return unless inheritable_setting.auth warn "Grape: #{config.app} is mounted under an API that declares authentication, but authentication " \ 'middleware does not wrap mounted Rack applications. Requests to this mount are not authenticated by Grape.' @@ -422,7 +413,7 @@ def build_response_cookies end def lint? - inheritable_setting.namespace_inheritable[:lint] || Grape.config.lint + inheritable_setting.lint? || Grape.config.lint end end end diff --git a/lib/grape/middleware/auth/dsl.rb b/lib/grape/middleware/auth/dsl.rb index eba95211a..52829cd22 100644 --- a/lib/grape/middleware/auth/dsl.rb +++ b/lib/grape/middleware/auth/dsl.rb @@ -5,12 +5,11 @@ module Middleware module Auth module DSL def auth(type = nil, *legacy_options, **options, &block) - namespace_inheritable = inheritable_setting.namespace_inheritable - return namespace_inheritable[:auth] unless type + return inheritable_setting.auth unless type options = merge_legacy_auth_options(:auth, legacy_options, options) - namespace_inheritable[:auth] = { type: type.to_sym, proc: block }.merge!(options) - use Grape::Middleware::Auth::Base, namespace_inheritable[:auth] + inheritable_setting.auth = { type: type.to_sym, proc: block }.merge!(options) + use Grape::Middleware::Auth::Base, inheritable_setting.auth end # Add HTTP Basic authorization to the API. 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..ba1bc7b05 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -5,17 +5,17 @@ module Util # A branchable, inheritable settings object which can store both stackable # and inheritable values (see InheritableValues and StackableValues). class InheritableSetting - attr_reader :route, :namespace, :namespace_inheritable, :namespace_stackable, :parent - - # Lazy-allocated; +api_class+ and +point_in_time_copies+ are rarely - # written on most settings layers, so don't pay for a Hash/Array each. - def api_class - @api_class ||= {} - end + # Maps the callbacks DSL method names to their pluralized + # namespace-stackable storage keys (see #callbacks / #add_callback). + CALLBACK_STORE_KEYS = { + before: :befores, + before_validation: :before_validations, + after_validation: :after_validations, + after: :afters, + finally: :finallies + }.freeze - def point_in_time_copies - @point_in_time_copies ||= [] - end + attr_reader :route, :namespace, :namespace_inheritable, :namespace_stackable, :parent # Retrieve global settings. def self.global @@ -39,7 +39,7 @@ def initialize @namespace_inheritable = InheritableValues.new @namespace_stackable = StackableValues.new @parent = nil - # @api_class, @point_in_time_copies and @rescue_handler_maps stay nil until first access. + @point_in_time_copies = nil end # Return the class-level global properties. @@ -69,12 +69,25 @@ def inherit_from(parent) # which were made. def point_in_time_copy new_setting = self.class.new - point_in_time_copies << new_setting + (@point_in_time_copies ||= []) << new_setting new_setting.copy_state_from(self) new_setting.inherit_from(parent) new_setting end + # Fork a point-in-time copy prepared for a freshly-built endpoint: the + # declared params and validations accumulated by the surrounding scopes + # are snapshotted into the copy's per-route settings, since the + # namespace stacks are wiped between routes (see #reset_validations!), + # and request-serving defaults are applied. + def point_in_time_copy_for_endpoint + copy = point_in_time_copy + copy.route[:declared_params] = copy.declared_params.flatten + copy.route[:validations] = copy.validations.dup + copy.default_error_status ||= 500 + copy + end + # Resets the instance store of per-route settings. # @api private def route_end @@ -106,6 +119,405 @@ def namespace_stackable_with_hash(key) data.each_with_object({}) { |value, result| result.deep_merge!(value) } end + # Validator instances registered by +params+ and +contract+ blocks, + # outermost scope first. Record them with #add_validation; the backing + # store is an internal detail. + def validations + namespace_stackable[:validations] + end + + def add_validation(validator) + namespace_stackable[:validations] = validator + end + + # Declared-params entries registered by +params+ blocks, one Array per + # scope, outermost scope first. Record them with #add_declared_params; + # the backing store is an internal detail. + def declared_params + namespace_stackable[:declared_params] + end + + def add_declared_params(params) + namespace_stackable[:declared_params] = params + end + + # Param documentation recorded by +params+ blocks (see + # Validations::ParamsDocumentation) as one attribute-name => details + # Hash per scope, deep-merged on read; nil when nothing is documented. + # Record entries with #add_params_documentation; the backing store is + # an internal detail. + def params_documentation + namespace_stackable_with_hash(:params) + end + + def add_params_documentation(documented_attrs) + namespace_stackable[:params] = documented_attrs + end + + # Drops this scope's own validations, declared params and params + # documentation once an endpoint has consumed them (see + # +reset_validations!+ in DSL::Validations). Inherited entries are kept. + def reset_validations! + namespace_stackable.delete(:declared_params, :params, :validations) + end + + # Reusable +params :name do ... end+ blocks defined in helpers, as one + # name => block Hash per scope, deep-merged on read; nil when none are + # defined. Consumed by +use+. Record entries with #add_named_params; + # the backing store is an internal detail. + def named_params + namespace_stackable_with_hash(:named_params) + end + + def add_named_params(named_params) + namespace_stackable[:named_params] = named_params + end + + # Filter blocks registered by the callbacks DSL (see DSL::Callbacks), + # as a callback-name => blocks Array Hash keyed by the DSL method names + # (+:before+, +:before_validation+, +:after_validation+, +:after+, + # +:finally+), outermost scope first. Record them with #add_callback; + # the backing store is an internal detail. + def callbacks + CALLBACK_STORE_KEYS.transform_values { |store_key| namespace_stackable[store_key] } + end + + def add_callback(callback_name, block) + namespace_stackable[CALLBACK_STORE_KEYS.fetch(callback_name)] = block + end + + # Response-shaping options recorded by +rescue_from+ (see + # DSL::RescueOptions): every +rescue_from+ stacks one entry and the + # nearest scope's latest registration wins on read; nil when + # +rescue_from+ was never called. Record them with #add_rescue_options; + # the backing store is an internal detail. + def rescue_options + namespace_stackable[:rescue_options].last + end + + def add_rescue_options(options) + namespace_stackable[:rescue_options] = options + end + + # Meta-selector registrations from +rescue_from :all+, + # +:grape_exceptions+ and +:internal_grape_exceptions+ (see + # DSL::RequestResponse#rescue_from): each records its handler (nil to + # use the built-in one) and flips the flags the error middleware reads + # through #rescue_all? / #rescue_grape_exceptions?; the backing store + # is an internal detail. + def add_all_rescue_handler(handler) + namespace_inheritable[:rescue_all] = true + namespace_inheritable[:all_rescue_handler] = handler + end + + def add_grape_exceptions_rescue_handler(handler) + namespace_inheritable[:rescue_all] = true + namespace_inheritable[:rescue_grape_exceptions] = true + namespace_inheritable[:grape_exceptions_rescue_handler] = handler + end + + def add_internal_grape_exceptions_rescue_handler(handler) + namespace_inheritable[:internal_grape_exceptions_rescue_handler] = handler + end + + def rescue_all? + namespace_inheritable[:rescue_all] == true + end + + def rescue_grape_exceptions? + namespace_inheritable[:rescue_grape_exceptions] == true + end + + def all_rescue_handler + namespace_inheritable[:all_rescue_handler] + end + + def grape_exceptions_rescue_handler + namespace_inheritable[:grape_exceptions_rescue_handler] + end + + def internal_grape_exceptions_rescue_handler + namespace_inheritable[:internal_grape_exceptions_rescue_handler] + end + + # Content negotiation registries recorded by the request/response DSL + # (see DSL::RequestResponse): the content-type registry (+content_type+ + # and +format+), and the formatter, parser and error-formatter handler + # maps. Each registration stacks one single-entry Hash, deep-merged on + # read so a nested scope's registration wins; readers return nil when + # nothing is registered. Record entries with the corresponding +add_*+ + # writer; the backing store is an internal detail. + def content_types + namespace_stackable_with_hash(:content_types) + end + + def add_content_type(format, content_type) + namespace_stackable[:content_types] = { format => content_type } + end + + def formatters + namespace_stackable_with_hash(:formatters) + end + + def add_formatter(content_type, formatter) + namespace_stackable[:formatters] = { content_type => formatter } + end + + def parsers + namespace_stackable_with_hash(:parsers) + end + + def add_parser(content_type, parser) + namespace_stackable[:parsers] = { content_type => parser } + end + + def error_formatters + namespace_stackable_with_hash(:error_formatters) + end + + def add_error_formatter(format, formatter) + namespace_stackable[:error_formatters] = { format => formatter } + end + + # Model-class => entity-class registrations from +represent+ (see + # DSL::RequestResponse), one single-entry Hash per registration, + # deep-merged on read so a nested scope's registration wins; nil when + # none are registered. Record them with #add_representation; the + # backing store is an internal detail. + def representations + namespace_stackable_with_hash(:representations) + end + + def add_representation(model_class, entity_class) + namespace_stackable[:representations] = { model_class => entity_class } + end + + # Middleware specs recorded by the middleware DSL (+use+, +insert+, + # +insert_before+, +insert_after+; see DSL::Middleware), one + # [operation, *arguments] Array per registration, outermost scope + # first. Record them with #add_middleware; the backing store is an + # internal detail. + def middleware + namespace_stackable[:middleware] + end + + def add_middleware(operation_with_arguments) + namespace_stackable[:middleware] = operation_with_arguments + end + + # Helper modules registered by +helpers+ blocks and modules (see + # DSL::Helpers), outermost scope first. Record them with #add_helper; + # the backing store is an internal detail. + def helpers + namespace_stackable[:helpers] + end + + def add_helper(mod) + namespace_stackable[:helpers] = mod + 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 + + # Dry::Schema key maps registered by +contract+ blocks (see + # Validations::ContractScope), one per contract, outermost scope first; + # +declared+ uses them to write coerced params back under their + # declared keys. Record them with #add_contract_key_map; the backing + # store is an internal detail. + def contract_key_maps + namespace_stackable[:contract_key_map] + end + + def add_contract_key_map(key_map) + namespace_stackable[:contract_key_map] = key_map + end + + # Serialization and error-response defaults recorded by the + # request/response DSL's get-or-set methods (see DSL::RequestResponse): + # +format+ is the enforced API format, +default_format+ the fallback + # used when a request doesn't specify one, and + # +default_error_formatter+ / +default_error_status+ shape error + # responses. Nearest-wins scalars — a nested scope's assignment + # overrides an inherited one, hence plain +=+ writers rather than the + # +add_*+ writers used for stackable registrations. Readers return nil + # when never set (Endpoint applies the request-serving fallbacks); the + # backing store is an internal detail. + def format + namespace_inheritable[:format] + end + + def format=(format) + namespace_inheritable[:format] = format + end + + def default_format + namespace_inheritable[:default_format] + end + + def default_format=(default_format) + namespace_inheritable[:default_format] = default_format + end + + def default_error_formatter + namespace_inheritable[:default_error_formatter] + end + + def default_error_formatter=(formatter) + namespace_inheritable[:default_error_formatter] = formatter + end + + def default_error_status + namespace_inheritable[:default_error_status] + end + + def default_error_status=(status) + namespace_inheritable[:default_error_status] = status + 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 + + # Scope flags flipped by the routing DSL's bang methods (see + # DSL::Routing#do_not_route_head! and friends; Validations::OneofCollector + # also flips +do_not_document!+): once set in a scope they apply to it + # and everything nested under it. Readers return false when never set; + # the backing store is an internal detail. + def do_not_route_head! + namespace_inheritable[:do_not_route_head] = true + end + + def do_not_route_head? + namespace_inheritable[:do_not_route_head] == true + end + + def do_not_route_options! + namespace_inheritable[:do_not_route_options] = true + end + + def do_not_route_options? + namespace_inheritable[:do_not_route_options] == true + end + + def do_not_document! + namespace_inheritable[:do_not_document] = true + end + + def do_not_document? + namespace_inheritable[:do_not_document] == true + end + + def lint! + namespace_inheritable[:lint] = true + end + + def lint? + namespace_inheritable[:lint] == true + end + + # The params-builder strategy set by +build_with+ (both the + # API-level DSL::Routing#build_with and the params-block + # DSL::Parameters#build_with write it), consumed when the endpoint + # builds its Grape::Request. Nearest-wins scalar; nil when never set; + # the backing store is an internal detail. + def build_params_with + namespace_inheritable[:build_params_with] + end + + def build_params_with=(strategy) + namespace_inheritable[:build_params_with] = strategy + end + + # The authentication configuration Hash recorded by the +auth+ DSL + # (see Middleware::Auth::DSL): {type:, proc:, **options}. Nearest-wins + # scalar; nil when no authenticator is declared — Endpoint uses that + # to warn about unauthenticated bare Rack mounts; the backing store is + # an internal detail. + def auth + namespace_inheritable[:auth] + end + + def auth=(auth_options) + namespace_inheritable[:auth] = auth_options + 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. @@ -150,7 +562,6 @@ def copy_state_from(source) @namespace_stackable = source.namespace_stackable.clone @rescue_handler_maps = source.rescue_handler_maps&.dup @route = source.route.clone - @api_class = source.api_class end end end diff --git a/lib/grape/validations/contract_scope.rb b/lib/grape/validations/contract_scope.rb index 07db7132d..e8f59e5f5 100644 --- a/lib/grape/validations/contract_scope.rb +++ b/lib/grape/validations/contract_scope.rb @@ -20,8 +20,8 @@ def initialize(api, contract = nil, &block) key_map = contract.key_map end - api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map - api.inheritable_setting.namespace_stackable[:validations] = Validators::ContractScopeValidator.new(schema: contract) + api.inheritable_setting.add_contract_key_map(key_map) + api.inheritable_setting.add_validation(Validators::ContractScopeValidator.new(schema: contract)) end end end diff --git a/lib/grape/validations/oneof_collector.rb b/lib/grape/validations/oneof_collector.rb index 84fa32d08..b8814e7d3 100644 --- a/lib/grape/validations/oneof_collector.rb +++ b/lib/grape/validations/oneof_collector.rb @@ -8,31 +8,27 @@ module Validations # the real API. Exposes only the slice of the API surface that # ParamsScope and its helpers touch during definition. class OneofCollector + extend Forwardable + attr_reader :inheritable_setting + def_delegator :@inheritable_setting, :validations + def initialize @inheritable_setting = Grape::Util::InheritableSetting.new - @inheritable_setting.namespace_inheritable[:do_not_document] = true + @inheritable_setting.do_not_document! end def configuration nil end - def validators - inheritable_setting.namespace_stackable[:validations] - end - - def declared_params - inheritable_setting.namespace_stackable[:declared_params] - end - # Evaluate +variant_block+ in a fresh +ParamsScope+ backed by a new # collector and return the validators that the block registered. def self.collect(variant_block) collector = new ParamsScope.new(api: collector, type: Hash, &variant_block) - collector.validators + collector.validations end end end diff --git a/lib/grape/validations/params_documentation.rb b/lib/grape/validations/params_documentation.rb index a17e80b0d..16db11c2d 100644 --- a/lib/grape/validations/params_documentation.rb +++ b/lib/grape/validations/params_documentation.rb @@ -6,12 +6,12 @@ module Validations # +ValidationsSpec+; never mutates the user's validations hash. module ParamsDocumentation def document_params(attrs, spec) - return if @api.inheritable_setting.namespace_inheritable[:do_not_document] + return if @api.inheritable_setting.do_not_document? documented_attrs = attrs.to_h do |name| [full_name(name), extract_details(spec)] end - @api.inheritable_setting.namespace_stackable[:params] = documented_attrs + @api.inheritable_setting.add_params_documentation(documented_attrs) end private diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index 8ab8a2ecb..02438c080 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -316,7 +316,7 @@ def configure_declared_params push_renamed_param(full_path, @element_renamed) if @element_renamed return @parent.push_declared_params [{ @element => @declared_params }] if nested? - @api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params + @api.inheritable_setting.add_declared_params(@declared_params) ensure @declared_params = nil end @@ -400,7 +400,7 @@ def validate(type, options, attrs, required, opts) self, opts ) - @api.inheritable_setting.namespace_stackable[:validations] = validator_instance + @api.inheritable_setting.add_validation(validator_instance) end def all_element_blank?(scoped_params) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 79005c7ea..b2cfb1ef4 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -140,7 +140,7 @@ dummy_presenter_klass = Class.new represent_object = Class.new subject.represent represent_object, with: dummy_presenter_klass - expect(subject.inheritable_setting.namespace_stackable[:representations]).to eq([{ represent_object => dummy_presenter_klass }]) + expect(subject.inheritable_setting.representations).to eq(represent_object => dummy_presenter_klass) end end diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index 8edb2e398..03d1d6f98 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -15,28 +15,35 @@ describe '.before' do it 'adds a block to "before"' do subject.before(&proc) - expect(subject.inheritable_setting.namespace_stackable[:befores]).to eq([proc]) + expect(subject.inheritable_setting.callbacks[:before]).to eq([proc]) end end describe '.before_validation' do it 'adds a block to "before_validation"' do subject.before_validation(&proc) - expect(subject.inheritable_setting.namespace_stackable[:before_validations]).to eq([proc]) + expect(subject.inheritable_setting.callbacks[:before_validation]).to eq([proc]) end end describe '.after_validation' do it 'adds a block to "after_validation"' do subject.after_validation(&proc) - expect(subject.inheritable_setting.namespace_stackable[:after_validations]).to eq([proc]) + expect(subject.inheritable_setting.callbacks[:after_validation]).to eq([proc]) end end describe '.after' do it 'adds a block to "after"' do subject.after(&proc) - expect(subject.inheritable_setting.namespace_stackable[:afters]).to eq([proc]) + expect(subject.inheritable_setting.callbacks[:after]).to eq([proc]) + end + end + + describe '.finally' do + it 'adds a block to "finally"' do + subject.finally(&proc) + expect(subject.inheritable_setting.callbacks[:finally]).to eq([proc]) end end end diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index f457c51a5..33567f210 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -9,7 +9,7 @@ extend Grape::DSL::Settings def self.mods - inheritable_setting.namespace_stackable[:helpers] + inheritable_setting.helpers end def self.first_mod diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index dd828638c..48e0bef77 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -54,7 +54,7 @@ def header(key = nil, val = nil) describe 'default_error_status' do before do - subject.inheritable_setting.namespace_inheritable[:default_error_status] = 500 + subject.inheritable_setting.default_error_status = 500 catch(:error) { subject.error! 'Unknown' } end diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index 00aee7cbe..7fb416233 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -17,28 +17,28 @@ describe '.use' do it 'adds a middleware with the right operation' do subject.use foo_middleware, :arg1, &proc - expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:use, foo_middleware, :arg1, proc]]) + expect(subject.inheritable_setting.middleware).to eq([[:use, foo_middleware, :arg1, proc]]) end end describe '.insert' do it 'adds a middleware with the right operation' do subject.insert 0, :arg1, &proc - expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert, 0, :arg1, proc]]) + expect(subject.inheritable_setting.middleware).to eq([[:insert, 0, :arg1, proc]]) end end describe '.insert_before' do it 'adds a middleware with the right operation' do subject.insert_before foo_middleware, :arg1, &proc - expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert_before, foo_middleware, :arg1, proc]]) + expect(subject.inheritable_setting.middleware).to eq([[:insert_before, foo_middleware, :arg1, proc]]) end end describe '.insert_after' do it 'adds a middleware with the right operation' do subject.insert_after foo_middleware, :arg1, &proc - expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert_after, foo_middleware, :arg1, proc]]) + expect(subject.inheritable_setting.middleware).to eq([[:insert_after, foo_middleware, :arg1, proc]]) end end diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 90e4a0905..f312ed962 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -53,17 +53,12 @@ def new_group_scope(group) end describe '#use' do - before do - allow_message_expectations_on_nil - allow(subject.api).to receive(:namespace_stackable).with(:named_params) - end - let(:options) { { option: 'value' } } let(:named_params) { { params_group: proc {} } } it 'calls processes associated with named params' do subject.api = Class.new { include Grape::DSL::Settings }.new - subject.api.inheritable_setting.namespace_stackable[:named_params] = named_params + subject.api.inheritable_setting.add_named_params(named_params) expect(subject).to receive(:instance_exec).with(options).and_yield subject.use :params_group, **options end diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index a5171a4b2..038d40176 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -16,7 +16,7 @@ describe '.default_format' do it 'sets the default format' do subject.default_format :format - expect(subject.inheritable_setting.namespace_inheritable[:default_format]).to eq(:format) + expect(subject.inheritable_setting.default_format).to eq(:format) end it 'returns the format without paramter' do @@ -28,29 +28,29 @@ describe '.format' do it 'sets a new format' do subject.format format - expect(subject.inheritable_setting.namespace_inheritable[:format]).to eq(format.to_sym) - expect(subject.inheritable_setting.namespace_inheritable[:default_error_formatter]).to eq(Grape::ErrorFormatter::Txt) + expect(subject.inheritable_setting.format).to eq(format.to_sym) + expect(subject.inheritable_setting.default_error_formatter).to eq(Grape::ErrorFormatter::Txt) end end describe '.formatter' do it 'sets the formatter for a content type' do subject.formatter c_type, :formatter - expect(subject.inheritable_setting.namespace_stackable[:formatters]).to eq([{ c_type.to_sym => :formatter }]) + expect(subject.inheritable_setting.formatters).to eq(c_type.to_sym => :formatter) end end describe '.parser' do it 'sets a parser for a content type' do subject.parser c_type, :parser - expect(subject.inheritable_setting.namespace_stackable[:parsers]).to eq([{ c_type.to_sym => :parser }]) + expect(subject.inheritable_setting.parsers).to eq(c_type.to_sym => :parser) end end describe '.default_error_formatter' do it 'sets a new error formatter' do subject.default_error_formatter :json - expect(subject.inheritable_setting.namespace_inheritable[:default_error_formatter]).to eq(Grape::ErrorFormatter::Json) + expect(subject.inheritable_setting.default_error_formatter).to eq(Grape::ErrorFormatter::Json) end end @@ -58,19 +58,19 @@ it 'sets a error_formatter' do format = 'txt' subject.error_formatter format, :error_formatter - expect(subject.inheritable_setting.namespace_stackable[:error_formatters]).to eq([{ format.to_sym => :error_formatter }]) + expect(subject.inheritable_setting.error_formatters).to eq(format.to_sym => :error_formatter) end it 'understands syntactic sugar' do subject.error_formatter format, with: :error_formatter - expect(subject.inheritable_setting.namespace_stackable[:error_formatters]).to eq([{ format.to_sym => :error_formatter }]) + expect(subject.inheritable_setting.error_formatters).to eq(format.to_sym => :error_formatter) end end describe '.content_type' do it 'sets a content type for a format' do subject.content_type format, c_type - expect(subject.inheritable_setting.namespace_stackable[:content_types]).to eq([{ format.to_sym => c_type }]) + expect(subject.inheritable_setting.content_types).to eq(format.to_sym => c_type) end end @@ -87,7 +87,7 @@ describe '.default_error_status' do it 'sets a default error status' do subject.default_error_status 500 - expect(subject.inheritable_setting.namespace_inheritable[:default_error_status]).to eq(500) + expect(subject.inheritable_setting.default_error_status).to eq(500) end end @@ -95,45 +95,29 @@ describe ':all' do it 'sets rescue all to true' do subject.rescue_from :all - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - all_rescue_handler: nil - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.all_rescue_handler).to be_nil end it 'sets given proc as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :all, rescue_handler_proc - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - all_rescue_handler: rescue_handler_proc - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.all_rescue_handler).to eq(rescue_handler_proc) end it 'sets given block as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :all, &rescue_handler_proc - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - all_rescue_handler: rescue_handler_proc - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.all_rescue_handler).to eq(rescue_handler_proc) end it 'sets a rescue handler declared through :with option' do with_block = -> { 'hello' } subject.rescue_from :all, with: with_block - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - all_rescue_handler: with_block - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.all_rescue_handler).to eq(with_block) end it 'abort if :with option value is not Symbol, String or Proc' do @@ -152,49 +136,42 @@ describe ':grape_exceptions' do it 'sets rescue all to true' do subject.rescue_from :grape_exceptions - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - rescue_grape_exceptions: true, - grape_exceptions_rescue_handler: nil - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.rescue_grape_exceptions?).to be(true) + expect(subject.inheritable_setting.grape_exceptions_rescue_handler).to be_nil end it 'sets given proc as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :grape_exceptions, rescue_handler_proc - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - rescue_grape_exceptions: true, - grape_exceptions_rescue_handler: rescue_handler_proc - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.rescue_grape_exceptions?).to be(true) + expect(subject.inheritable_setting.grape_exceptions_rescue_handler).to eq(rescue_handler_proc) end it 'sets given block as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :grape_exceptions, &rescue_handler_proc - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - rescue_grape_exceptions: true, - grape_exceptions_rescue_handler: rescue_handler_proc - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.rescue_grape_exceptions?).to be(true) + expect(subject.inheritable_setting.grape_exceptions_rescue_handler).to eq(rescue_handler_proc) end it 'sets a rescue handler declared through :with option' do with_block = -> { 'hello' } subject.rescue_from :grape_exceptions, with: with_block - expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( - { - rescue_all: true, - rescue_grape_exceptions: true, - grape_exceptions_rescue_handler: with_block - } - ) + expect(subject.inheritable_setting.rescue_all?).to be(true) + expect(subject.inheritable_setting.rescue_grape_exceptions?).to be(true) + expect(subject.inheritable_setting.grape_exceptions_rescue_handler).to eq(with_block) + end + end + + describe ':internal_grape_exceptions' do + it 'sets given proc as rescue handler without rescuing all' do + rescue_handler_proc = proc {} + subject.rescue_from :internal_grape_exceptions, rescue_handler_proc + expect(subject.inheritable_setting.internal_grape_exceptions_rescue_handler).to eq(rescue_handler_proc) + expect(subject.inheritable_setting.rescue_all?).to be(false) end end @@ -216,39 +193,39 @@ end describe 'list of exceptions is passed' do - let(:default_rescue_options) { [Grape::DSL::RescueOptions.new] } + let(:default_rescue_options) { Grape::DSL::RescueOptions.new } it 'sets hash of exceptions as rescue handlers' do subject.rescue_from StandardError expect(subject.inheritable_setting.rescue_handlers).to eq(StandardError => nil) - expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq(default_rescue_options) + expect(subject.inheritable_setting.rescue_options).to eq(default_rescue_options) end it 'rescues only base handlers if rescue_subclasses: false option is passed' do subject.rescue_from StandardError, rescue_subclasses: false expect(subject.inheritable_setting.base_only_rescue_handlers).to eq(StandardError => nil) - expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq(default_rescue_options) + expect(subject.inheritable_setting.rescue_options).to eq(default_rescue_options) end it 'sets given proc as rescue handler for each key in hash' do rescue_handler_proc = proc {} subject.rescue_from StandardError, rescue_handler_proc expect(subject.inheritable_setting.rescue_handlers).to eq(StandardError => rescue_handler_proc) - expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq(default_rescue_options) + expect(subject.inheritable_setting.rescue_options).to eq(default_rescue_options) end it 'sets given block as rescue handler for each key in hash' do rescue_handler_proc = proc {} subject.rescue_from StandardError, &rescue_handler_proc expect(subject.inheritable_setting.rescue_handlers).to eq(StandardError => rescue_handler_proc) - expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq(default_rescue_options) + expect(subject.inheritable_setting.rescue_options).to eq(default_rescue_options) end it 'sets a rescue handler declared through :with option for each key in hash' do with_block = -> { 'hello' } subject.rescue_from StandardError, with: with_block expect(subject.inheritable_setting.rescue_handlers).to eq(StandardError => with_block) - expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq(default_rescue_options) + expect(subject.inheritable_setting.rescue_options).to eq(default_rescue_options) end end end @@ -257,7 +234,7 @@ it 'sets a presenter for a class' do presenter = Class.new subject.represent :ThisClass, with: presenter - expect(subject.inheritable_setting.namespace_stackable[:representations]).to eq([{ ThisClass: presenter }]) + expect(subject.inheritable_setting.representations).to eq(ThisClass: presenter) end end end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 57afe0c47..9f5c2bf89 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 @@ -58,14 +58,14 @@ class << self describe '.do_not_route_head!' do it 'sets do not route head option' do subject.do_not_route_head! - expect(subject.inheritable_setting.namespace_inheritable[:do_not_route_head]).to be(true) + expect(subject.inheritable_setting.do_not_route_head?).to be(true) end end describe '.do_not_route_options!' do it 'sets do not route options option' do subject.do_not_route_options! - expect(subject.inheritable_setting.namespace_inheritable[:do_not_route_options]).to be(true) + expect(subject.inheritable_setting.do_not_route_options?).to be(true) end end @@ -152,7 +152,7 @@ class << self it 'generates correct endpoint options' do subject.inheritable_setting.route[:description] = { fiz: 'baz' } - subject.inheritable_setting.namespace_stackable[:params] = { nuz: 'naz' } + subject.inheritable_setting.add_params_documentation({ nuz: 'naz' }) expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options| expect(endpoint_options[:http_methods]).to eq :get diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index 213e62821..02f0f041b 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -52,16 +52,6 @@ end end - describe '#api_class' do - it 'is specific to the class' do - subject.api_class[:some_thing] = :foo_bar - parent.api_class[:some_thing] = :some_thing - - expect(subject.api_class[:some_thing]).to eq :foo_bar - expect(parent.api_class[:some_thing]).to eq :some_thing - end - end - describe '#namespace' do it 'sets a value until the end of a namespace' do subject.namespace[:some_thing] = :foo_bar @@ -150,13 +140,6 @@ end end - describe '#api_class' do - it 'is specific to the class' do - subject.api_class[:some_thing] = :foo_bar - expect(subject.api_class[:some_thing]).to eq :foo_bar - end - end - describe '#inherit_from' do it 'notifies clones' do new_settings = subject.point_in_time_copy @@ -169,8 +152,8 @@ describe '#point_in_time_copy' do let!(:cloned_obj) { subject.point_in_time_copy } - it 'resets point_in_time_copies' do - expect(cloned_obj.point_in_time_copies).to be_empty + it 'does not carry over the list of registered clones' do + expect(cloned_obj.instance_variable_get(:@point_in_time_copies)).to be_nil end it 'decouples namespace values' do @@ -209,7 +192,7 @@ end it 'adds itself to original as clone' do - expect(subject.point_in_time_copies).to include(cloned_obj) + expect(subject.instance_variable_get(:@point_in_time_copies)).to include(cloned_obj) end end diff --git a/spec/grape/validations/params_documentation_spec.rb b/spec/grape/validations/params_documentation_spec.rb index b6e1fdcab..b82074ba2 100644 --- a/spec/grape/validations/params_documentation_spec.rb +++ b/spec/grape/validations/params_documentation_spec.rb @@ -43,7 +43,7 @@ def spec_for(validations) except_values: [4, 5, 6] } subject.document_params(attrs, spec_for(validations)) - stored = api_double.inheritable_setting.namespace_stackable[:params].first + stored = api_double.inheritable_setting.params_documentation expect(stored.keys).to include('full_name_foo', 'full_name_bar') expect(stored['full_name_foo']).to include( required: true, @@ -64,12 +64,12 @@ def spec_for(validations) end before do - api_double.inheritable_setting.namespace_inheritable[:do_not_document] = true + api_double.inheritable_setting.do_not_document! end it 'does not store any documented params' do subject.document_params([:foo], spec_for(validations)) - expect(api_double.inheritable_setting.namespace_stackable[:params]).to be_empty + expect(api_double.inheritable_setting.params_documentation).to be_nil end it 'does not mutate the input validations' do @@ -86,7 +86,7 @@ def spec_for(validations) it 'does not raise an error' do expect { subject.document_params([:foo], spec_for(validations)) }.not_to raise_error - expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).to eq({ required: false }) + expect(api_double.inheritable_setting.params_documentation['full_name_foo']).to eq({ required: false }) end end @@ -97,7 +97,7 @@ def spec_for(validations) it 'uses description if desc is not present' do subject.document_params([:foo], spec_for(validations)) - expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo'][:desc]).to eq('desc2') + expect(api_double.inheritable_setting.params_documentation['full_name_foo'][:desc]).to eq('desc2') end end @@ -108,7 +108,7 @@ def spec_for(validations) it 'uses description if desc is not present' do subject.document_params([:foo], spec_for(validations)) - expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).to eq({ required: false }) + expect(api_double.inheritable_setting.params_documentation['full_name_foo']).to eq({ required: false }) end end @@ -119,7 +119,7 @@ def spec_for(validations) it 'does not include documentation' do subject.document_params([:foo], spec_for(validations)) - expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).not_to have_key(:documentation) + expect(api_double.inheritable_setting.params_documentation['full_name_foo']).not_to have_key(:documentation) end end @@ -130,7 +130,7 @@ def spec_for(validations) it 'sets type as nil' do subject.document_params([:foo], spec_for(validations)) - expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo'][:type]).to be_nil + expect(api_double.inheritable_setting.params_documentation['full_name_foo'][:type]).to be_nil end end end