From 38965817c002f52709e920a39890c230f3e76c68 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Sat, 17 Oct 2026 21:34:20 +0200 Subject: [PATCH] Hold every module to RuboCop's default length Metrics/ModuleLength was raised to 220; the two modules above the default of 100 are brought under it instead, and the override is dropped. DSL::Routing (218) also held the mounting and versioning DSLs, which the README documents as sections of their own. They move out, and Grape::API::Instance extends both: - DSL::Mounting: mount, mounted and given, the refresh/drop internals behind a re-mount, and the configuration-aware block evaluation. - DSL::Versioning: version, versions and cascade. Routing keeps routes, namespaces, route_param, scope, prefix and the route toggles, at 100. Their specs move with them. DSL::InsideRoute (101) answers `status` with a guard clause instead of a case/when whose else raised. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 --- .rubocop.yml | 3 - CHANGELOG.md | 1 + lib/grape/api/instance.rb | 2 + lib/grape/dsl/inside_route.rb | 8 +- lib/grape/dsl/mounting.rb | 149 ++++++++++++++++ lib/grape/dsl/routing.rb | 233 +------------------------- lib/grape/dsl/version_options.rb | 2 +- lib/grape/dsl/versioning.rb | 98 +++++++++++ lib/grape/util/inheritable_setting.rb | 4 +- spec/grape/dsl/mounting_spec.rb | 56 +++++++ spec/grape/dsl/routing_spec.rb | 71 +------- spec/grape/dsl/versioning_spec.rb | 29 ++++ 12 files changed, 342 insertions(+), 314 deletions(-) create mode 100644 lib/grape/dsl/mounting.rb create mode 100644 lib/grape/dsl/versioning.rb create mode 100644 spec/grape/dsl/mounting_spec.rb create mode 100644 spec/grape/dsl/versioning_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 70bc33b36..0f4f6c187 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -63,9 +63,6 @@ Metrics/ParameterLists: Metrics/MethodLength: Max: 32 -Metrics/ModuleLength: - Max: 220 - RSpec/ExampleLength: Max: 60 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9d0f36a..291361cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ * [#3033](https://github.com/ruby-grape/grape/pull/3033): Allocate less per request when presenting an entity or reading path params - [@ericproulx](https://github.com/ericproulx). * [#3034](https://github.com/ruby-grape/grape/pull/3034): Shrink each request's copy of its endpoint - [@ericproulx](https://github.com/ericproulx). * [#3038](https://github.com/ruby-grape/grape/pull/3038): Hold every method to RuboCop's default ABC size - [@ericproulx](https://github.com/ericproulx). +* [#3039](https://github.com/ruby-grape/grape/pull/3039): Hold every module to RuboCop's default length - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 4.0.1 (2026-09-15) diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb index 566e743a1..1c84209d2 100644 --- a/lib/grape/api/instance.rb +++ b/lib/grape/api/instance.rb @@ -15,6 +15,8 @@ class Instance extend Grape::DSL::Middleware extend Grape::DSL::RequestResponse extend Grape::DSL::Routing + extend Grape::DSL::Mounting + extend Grape::DSL::Versioning extend Grape::DSL::Helpers extend Grape::Middleware::Auth::DSL diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index 8e77f334d..746f2a75d 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -77,13 +77,9 @@ def redirect(url, permanent: false, body: nil) # @param status [Integer] The HTTP Status Code to return for this request. def status(status = nil) return @status || default_status if status.nil? + raise ArgumentError, 'status code must be Integer or Symbol' unless status.is_a?(Symbol) || status.is_a?(Integer) - case status - when Symbol, Integer - @status = Rack::Utils.status_code(status) - else - raise ArgumentError, 'status code must be Integer or Symbol' - end + @status = Rack::Utils.status_code(status) end # Set response content-type diff --git a/lib/grape/dsl/mounting.rb b/lib/grape/dsl/mounting.rb new file mode 100644 index 000000000..2283913aa --- /dev/null +++ b/lib/grape/dsl/mounting.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Grape + module DSL + module Mounting + def given(conditional_option, &) + return unless conditional_option + + mounted(&) + end + + def mounted(&block) + evaluate_as_instance_with_configuration(block, lazy: true) + end + + def mount(mounts, opts = {}) + mount_opts = opts + if opts[:refresh_already_mounted] + Grape.deprecator.warn('`refresh_already_mounted` is not a `mount` option and will be ignored in a future release.') + drop_endpoints_mounted_for(mounts) + # Dropped before the recursion below re-enters with the same options, + # so a Grape API does not warn once per mount and once per instance. + mount_opts = opts.except(:refresh_already_mounted) + end + + normalize_mounts(mounts).each_pair do |app, path| + if app.respond_to?(:mount_instance) + mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts) + next + end + + endpoints << Grape::Endpoint.new( + mounted_setting(app, path), + http_methods: '*', + path:, + app:, + anchor: false, + api: self + ) + end + end + + private + + # The settings the endpoint mounting +app+ at +path+ is built with. Past + # #mount's +mount_instance+ branch, a Grape app is an already instantiated + # Grape::API::Instance (vs. a bare Rack app): it inherits this API's + # settings and is built from its own. + def mounted_setting(app, path) + return inheritable_setting unless app.is_a?(Grape::Mountable) + + app.top_level_setting.add_mount_path(Grape::Util::PathNormalizer.call(path)) + app.inherit_settings(inheritable_setting) + app.change! + change! + app.top_level_setting + end + + # Re-mount +mounts+, replacing any endpoint already mounted for the same + # base API rather than adding a second one. Called by + # {Grape::API.refresh_mount_step} when a class-level method runs after the + # API was mounted. + # + # A bare Rack app is put back where it was mounted. It is routed as an + # ANY route, and those are tried in the order they were declared, so + # re-mounting it last let a catch-all declared after it answer first. A + # mounted Grape API still moves to the end: its routes answer for + # specific methods, where moving them would change which of two + # routes on the same path answers. + def refresh_mounted_api(mounts, opts = {}) + namespace = inheritable_setting.namespace_path + normalize_mounts(mounts).each_pair do |app, path| + position = endpoints.index { |endpoint| mounted_at?(endpoint, app, Array(path), namespace) } unless app.respond_to?(:mount_instance) + drop_endpoints_mounted_for(app => path) + mount({ app => path }, opts) + endpoints.insert(position, endpoints.pop) if position + end + end + + # A mounted Grape API is stored as the throwaway instance +mount+ built + # for it, never as the class that was written, so endpoints are matched on + # the base API both of them share -- and on where it was mounted, the + # path and the current namespace: the same API mounted at two paths, or + # in two namespaces, is two mounts, and refreshing one must leave the + # other in place. + def drop_endpoints_mounted_for(mounts) + namespace = inheritable_setting.namespace_path + normalize_mounts(mounts).each_pair do |app, path| + paths = Array(path) + endpoints.delete_if do |endpoint| + dropped = mounted_at?(endpoint, app, paths, namespace) + release_mount_instance(endpoint.mounted_app) if dropped + dropped + end + end + end + + # A Grape API keeps every instance +mount+ builds for it, so that steps + # recorded later replay onto each (see Grape::API::Instance.base=). Once + # the endpoint serving one is dropped, nothing serves it: its API lets go + # of it, and of the instances built for the APIs mounted inside it. + # Otherwise every refresh left another copy of each mounted API behind. + def release_mount_instance(mounted) + return unless mounted.respond_to?(:base) && mounted.base.respond_to?(:instances) + + mounted.base.instances.delete(mounted) + mounted.endpoints.each { |endpoint| release_mount_instance(endpoint.mounted_app) } + end + + # A bare app mounts at the root. The test is +Hash+ rather than + # +respond_to?(:each_pair)+ because a Struct or an OpenStruct answers that + # too, and neither can express an app => path mapping — their keys are + # member names. Reading one as a mapping would silently mount nonsense + # instead of mounting the app itself. + def normalize_mounts(mounts) + mounts.is_a?(Hash) ? mounts : { mounts => '/' } + end + + def mounted_at?(endpoint, app, paths, namespace) + same_mounted_app?(endpoint.mounted_app, app) && endpoint.path == paths && endpoint.namespace == namespace + end + + # Two mounts refer to the same app when they share the same base Grape + # API. +mount+ turns every mounted Grape API into a throwaway + # +mount_instance+ (a fresh +Class.new+ per mount), so object identity + # never holds across mounts; comparing the base is the real signal. + # Plain Rack apps have no base and are mounted as-is, so they fall back + # to object identity. + def same_mounted_app?(mounted, app) + return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base) + + mounted.equal?(app) + end + + def evaluate_as_instance_with_configuration(block, lazy: false) + lazy_block = Grape::Util::Lazy::Block.new do |configuration| + value_for_configuration = configuration + self.configuration = value_for_configuration.evaluate if value_for_configuration.is_a?(Grape::Util::Lazy::Base) + response = instance_eval(&block) + self.configuration = value_for_configuration + response + end + return lazy_block if @base && base_instance? && lazy + + lazy_block.evaluate_from(configuration) + end + end + end +end diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index 55fb0de31..0eeae3424 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -5,89 +5,6 @@ module DSL module Routing attr_reader :endpoints - def given(conditional_option, &) - return unless conditional_option - - mounted(&) - end - - def mounted(&block) - evaluate_as_instance_with_configuration(block, lazy: true) - end - - # Called without a value, answers whether the API cascades: the value - # given here, else the +cascade:+ of +version+, else true. The compiled - # API reads it here too; this reader used to skip +version+, and so - # answered true for an API that did not cascade. - def cascade(value = nil) - return inheritable_setting.cascade = value unless value.nil? - - setting = inheritable_setting - return setting.cascade if setting.cascade_defined? - return setting.version_options.cascade if setting.version_options - - true - end - - # Specify an API version. - # - # Called without arguments, returns the most recently declared version - # (or +nil+). Called with one or more version strings, registers them - # and stores a {Grape::DSL::VersionOptions} value object on the - # inheritable settings; when given a block, the registration applies - # within a nested namespace. - # - # @example API with legacy support. - # class MyAPI < Grape::API - # version 'v2' - # - # get '/main' do - # {some: 'data'} - # end - # - # version 'v1' do - # get '/main' do - # {legacy: 'data'} - # end - # end - # end - # - # @param args [Array] one or more version identifiers. - # @param using [Symbol] versioning strategy — one of +:path+ (default), - # +:header+, +:param+, or +:accept_version_header+. - # @param cascade [Boolean] forward to subsequent routes via the - # +X-Cascade+ header on version mismatch. Defaults to +true+. - # @param parameter [String] name of the query/body parameter that - # carries the version when +using: :param+. Defaults to +'apiver'+. - # @param strict [Boolean] reject requests that don't supply a usable - # version (header strategies). Defaults to +false+. - # @param vendor [String, nil] vendor segment for the +:header+ - # strategy (+application/vnd.-+); required when - # +using: :header+. - # @yield optional block to scope routes under this version. - # @return [String, nil] the most recently declared version. - # @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+ - # is supplied without a +:vendor+. - def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block) - return @versions&.last if args.empty? - - options = build_version_options(using:, cascade:, parameter:, strict:, vendor:) - requested_versions = args.flatten.map(&:to_s) - - @versions = versions | requested_versions - - if block - within_namespace do - apply_version(requested_versions, options) - instance_eval(&block) - end - else - apply_version(requested_versions, options) - end - - @versions.last - end - # Define a root URL prefix for your entire API. def prefix(prefix = nil) return inheritable_setting.root_prefix if prefix.nil? @@ -100,9 +17,7 @@ def prefix(prefix = nil) # @param _name [Symbol] Purely placebo, just allows to name the scope to # make the code more readable. def scope(_name = nil, &block) - within_namespace do - nest(block) - end + within_namespace { nest(block) } end def build_with(build_with) @@ -127,33 +42,6 @@ def do_not_document! inheritable_setting.do_not_document! end - def mount(mounts, opts = {}) - mount_opts = opts - if opts[:refresh_already_mounted] - Grape.deprecator.warn('`refresh_already_mounted` is not a `mount` option and will be ignored in a future release.') - drop_endpoints_mounted_for(mounts) - # Dropped before the recursion below re-enters with the same options, - # so a Grape API does not warn once per mount and once per instance. - mount_opts = opts.except(:refresh_already_mounted) - end - - normalize_mounts(mounts).each_pair do |app, path| - if app.respond_to?(:mount_instance) - mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts) - next - end - - endpoints << Grape::Endpoint.new( - mounted_setting(app, path), - http_methods: '*', - path:, - app:, - anchor: false, - api: self - ) - end - end - # Defines a route that will be recognized # by the Grape API. # @@ -256,11 +144,6 @@ def route_param(param, requirements: nil, type: nil, **, &) namespace(":#{param}", requirements: param_requirements, **, &) end - # @return array of defined versions - def versions - @versions ||= [] - end - private # Requirements are keyed by param name and merged across namespaces, so @@ -290,31 +173,6 @@ def prepare_route_options(description, route_options) description_options.deep_merge!(route_options) end - def build_version_options(using:, vendor:, **) - raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil? - - VersionOptions.new(using:, vendor:, **) - end - - def apply_version(versions, options) - inheritable_setting.version = versions - inheritable_setting.version_options = options - end - - # The settings the endpoint mounting +app+ at +path+ is built with. Past - # #mount's +mount_instance+ branch, a Grape app is an already instantiated - # Grape::API::Instance (vs. a bare Rack app): it inherits this API's - # settings and is built from its own. - def mounted_setting(app, path) - return inheritable_setting unless app.is_a?(Grape::Mountable) - - app.top_level_setting.add_mount_path(Grape::Util::PathNormalizer.call(path)) - app.inherit_settings(inheritable_setting) - app.change! - change! - app.top_level_setting - end - # Remove all defined routes. def reset_routes! endpoints.each(&:reset_routes!) @@ -325,82 +183,6 @@ def reset_endpoints! @endpoints = [] end - # Re-mount +mounts+, replacing any endpoint already mounted for the same - # base API rather than adding a second one. Called by - # {Grape::API.refresh_mount_step} when a class-level method runs after the - # API was mounted. - # - # A bare Rack app is put back where it was mounted. It is routed as an - # ANY route, and those are tried in the order they were declared, so - # re-mounting it last let a catch-all declared after it answer first. A - # mounted Grape API still moves to the end: its routes answer for - # specific methods, where moving them would change which of two - # routes on the same path answers. - def refresh_mounted_api(mounts, opts = {}) - namespace = inheritable_setting.namespace_path - normalize_mounts(mounts).each_pair do |app, path| - position = endpoints.index { |endpoint| mounted_at?(endpoint, app, Array(path), namespace) } unless app.respond_to?(:mount_instance) - drop_endpoints_mounted_for(app => path) - mount({ app => path }, opts) - endpoints.insert(position, endpoints.pop) if position - end - end - - # A mounted Grape API is stored as the throwaway instance +mount+ built - # for it, never as the class that was written, so endpoints are matched on - # the base API both of them share -- and on where it was mounted, the - # path and the current namespace: the same API mounted at two paths, or - # in two namespaces, is two mounts, and refreshing one must leave the - # other in place. - def drop_endpoints_mounted_for(mounts) - namespace = inheritable_setting.namespace_path - normalize_mounts(mounts).each_pair do |app, path| - paths = Array(path) - endpoints.delete_if do |endpoint| - dropped = mounted_at?(endpoint, app, paths, namespace) - release_mount_instance(endpoint.mounted_app) if dropped - dropped - end - end - end - - # A Grape API keeps every instance +mount+ builds for it, so that steps - # recorded later replay onto each (see Grape::API::Instance.base=). Once - # the endpoint serving one is dropped, nothing serves it: its API lets go - # of it, and of the instances built for the APIs mounted inside it. - # Otherwise every refresh left another copy of each mounted API behind. - def release_mount_instance(mounted) - return unless mounted.respond_to?(:base) && mounted.base.respond_to?(:instances) - - mounted.base.instances.delete(mounted) - mounted.endpoints.each { |endpoint| release_mount_instance(endpoint.mounted_app) } - end - - # A bare app mounts at the root. The test is +Hash+ rather than - # +respond_to?(:each_pair)+ because a Struct or an OpenStruct answers that - # too, and neither can express an app => path mapping — their keys are - # member names. Reading one as a mapping would silently mount nonsense - # instead of mounting the app itself. - def normalize_mounts(mounts) - mounts.is_a?(Hash) ? mounts : { mounts => '/' } - end - - def mounted_at?(endpoint, app, paths, namespace) - same_mounted_app?(endpoint.mounted_app, app) && endpoint.path == paths && endpoint.namespace == namespace - end - - # Two mounts refer to the same app when they share the same base Grape - # API. +mount+ turns every mounted Grape API into a throwaway - # +mount_instance+ (a fresh +Class.new+ per mount), so object identity - # never holds across mounts; comparing the base is the real signal. - # Plain Rack apps have no base and are mounted as-is, so they fall back - # to object identity. - def same_mounted_app?(mounted, app) - return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base) - - mounted.equal?(app) - end - # Execute first the provided block, then each of the # block passed in. Allows for simple 'before' setups # of settings stack pushes. @@ -412,19 +194,6 @@ def nest(*blocks, &block) blocks.each { |b| evaluate_as_instance_with_configuration(b) } reset_validations! end - - def evaluate_as_instance_with_configuration(block, lazy: false) - lazy_block = Grape::Util::Lazy::Block.new do |configuration| - value_for_configuration = configuration - self.configuration = value_for_configuration.evaluate if value_for_configuration.is_a?(Grape::Util::Lazy::Base) - response = instance_eval(&block) - self.configuration = value_for_configuration - response - end - return lazy_block if @base && base_instance? && lazy - - lazy_block.evaluate_from(configuration) - end end end end diff --git a/lib/grape/dsl/version_options.rb b/lib/grape/dsl/version_options.rb index 03ae9b096..cddccd13c 100644 --- a/lib/grape/dsl/version_options.rb +++ b/lib/grape/dsl/version_options.rb @@ -3,7 +3,7 @@ module Grape module DSL # Immutable value object holding the resolved options from - # +Grape::DSL::Routing#version+. Stored on the inheritable settings as + # +Grape::DSL::Versioning#version+. Stored on the inheritable settings as # +Grape::Util::InheritableSetting#version_options+ and read by internal call # sites (`Path`, `Endpoint`, `API::Instance`, # `Middleware::Versioner::Base`) via accessors. diff --git a/lib/grape/dsl/versioning.rb b/lib/grape/dsl/versioning.rb new file mode 100644 index 000000000..9c04e504d --- /dev/null +++ b/lib/grape/dsl/versioning.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Grape + module DSL + module Versioning + # Called without a value, answers whether the API cascades: the value + # given here, else the +cascade:+ of +version+, else true. The compiled + # API reads it here too; this reader used to skip +version+, and so + # answered true for an API that did not cascade. + def cascade(value = nil) + return inheritable_setting.cascade = value unless value.nil? + + setting = inheritable_setting + return setting.cascade if setting.cascade_defined? + return setting.version_options.cascade if setting.version_options + + true + end + + # Specify an API version. + # + # Called without arguments, returns the most recently declared version + # (or +nil+). Called with one or more version strings, registers them + # and stores a {Grape::DSL::VersionOptions} value object on the + # inheritable settings; when given a block, the registration applies + # within a nested namespace. + # + # @example API with legacy support. + # class MyAPI < Grape::API + # version 'v2' + # + # get '/main' do + # {some: 'data'} + # end + # + # version 'v1' do + # get '/main' do + # {legacy: 'data'} + # end + # end + # end + # + # @param args [Array] one or more version identifiers. + # @param using [Symbol] versioning strategy — one of +:path+ (default), + # +:header+, +:param+, or +:accept_version_header+. + # @param cascade [Boolean] forward to subsequent routes via the + # +X-Cascade+ header on version mismatch. Defaults to +true+. + # @param parameter [String] name of the query/body parameter that + # carries the version when +using: :param+. Defaults to +'apiver'+. + # @param strict [Boolean] reject requests that don't supply a usable + # version (header strategies). Defaults to +false+. + # @param vendor [String, nil] vendor segment for the +:header+ + # strategy (+application/vnd.-+); required when + # +using: :header+. + # @yield optional block to scope routes under this version. + # @return [String, nil] the most recently declared version. + # @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+ + # is supplied without a +:vendor+. + def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block) + return @versions&.last if args.empty? + + options = build_version_options(using:, cascade:, parameter:, strict:, vendor:) + requested_versions = args.flatten.map(&:to_s) + + @versions = versions | requested_versions + + if block + within_namespace do + apply_version(requested_versions, options) + instance_eval(&block) + end + else + apply_version(requested_versions, options) + end + + @versions.last + end + + # @return array of defined versions + def versions + @versions ||= [] + end + + private + + def build_version_options(using:, vendor:, **) + raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil? + + VersionOptions.new(using:, vendor:, **) + end + + def apply_version(versions, options) + inheritable_setting.version = versions + inheritable_setting.version_options = options + end + end + end +end diff --git a/lib/grape/util/inheritable_setting.rb b/lib/grape/util/inheritable_setting.rb index a3c3902c8..e86238fd3 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -485,7 +485,7 @@ def namespace_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 + # top-level settings by +mount+ (see DSL::Mounting). Reading returns the # outermost mount path — nil when the API is not mounted; the backing # store is an internal detail. def mount_path @@ -558,7 +558,7 @@ def default_error_status=(status) set_inheritable(:default_error_status, status) end - # Versioning state recorded by the routing DSL (see DSL::Routing): + # Versioning state recorded by the DSL (see DSL::Versioning and 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+. diff --git a/spec/grape/dsl/mounting_spec.rb b/spec/grape/dsl/mounting_spec.rb new file mode 100644 index 000000000..1bf9754e5 --- /dev/null +++ b/spec/grape/dsl/mounting_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +describe Grape::DSL::Mounting do + describe '.mount' do + it 'mounts on a nested path' do + subject = Class.new(Grape::API) + app1 = Class.new(Grape::API) do + get '/' do + return_no_content + end + end + app2 = Class.new(Grape::API) do + get '/' do + return_no_content + end + end + subject.mount app1 => '/app1' + app1.mount app2 => '/app2' + + env = Rack::MockRequest.env_for('/app1', method: Rack::GET) + response = Rack::MockResponse[*subject.call(env)] + + expect(response).to be_no_content + + env = Rack::MockRequest.env_for('/app1/app2', method: Rack::GET) + response = Rack::MockResponse[*subject.call(env)] + + expect(response).to be_no_content + end + + it 'mounts multiple routes at once' do + base_app = Class.new(Grape::API) + app1 = Class.new(Grape::API) do + get '/' do + return_no_content + end + end + app2 = Class.new(Grape::API) do + get '/' do + return_no_content + end + end + base_app.mount(app1 => '/app1', app2 => '/app2') + + env = Rack::MockRequest.env_for('/app1', method: Rack::GET) + response = Rack::MockResponse[*base_app.call(env)] + + expect(response).to be_no_content + + env = Rack::MockRequest.env_for('/app2', method: Rack::GET) + response = Rack::MockResponse[*base_app.call(env)] + + expect(response).to be_no_content + end + end +end diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index ea4867c51..411ce094e 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -6,6 +6,7 @@ let(:dummy_class) do Class.new do extend Grape::DSL::Routing + extend Grape::DSL::Mounting extend Grape::DSL::Settings extend Grape::DSL::Validations @@ -20,15 +21,6 @@ class << self let(:options) { { a: :b } } let(:path) { '/dummy' } - describe '.version' do - it 'sets a version for route' do - version = 'v1' - expect(subject.version(version)).to eq(version) - expect(subject.inheritable_setting.version).to eq([version]) - expect(subject.inheritable_setting.version_options).to eq(Grape::DSL::VersionOptions.new) - end - end - describe '.prefix' do it 'sets a prefix for route' do prefix = '/api' @@ -69,59 +61,6 @@ class << self end end - describe '.mount' do - it 'mounts on a nested path' do - subject = Class.new(Grape::API) - app1 = Class.new(Grape::API) do - get '/' do - return_no_content - end - end - app2 = Class.new(Grape::API) do - get '/' do - return_no_content - end - end - subject.mount app1 => '/app1' - app1.mount app2 => '/app2' - - env = Rack::MockRequest.env_for('/app1', method: Rack::GET) - response = Rack::MockResponse[*subject.call(env)] - - expect(response).to be_no_content - - env = Rack::MockRequest.env_for('/app1/app2', method: Rack::GET) - response = Rack::MockResponse[*subject.call(env)] - - expect(response).to be_no_content - end - - it 'mounts multiple routes at once' do - base_app = Class.new(Grape::API) - app1 = Class.new(Grape::API) do - get '/' do - return_no_content - end - end - app2 = Class.new(Grape::API) do - get '/' do - return_no_content - end - end - base_app.mount(app1 => '/app1', app2 => '/app2') - - env = Rack::MockRequest.env_for('/app1', method: Rack::GET) - response = Rack::MockResponse[*base_app.call(env)] - - expect(response).to be_no_content - - env = Rack::MockRequest.env_for('/app2', method: Rack::GET) - response = Rack::MockResponse[*base_app.call(env)] - - expect(response).to be_no_content - end - end - describe '.route' do before do allow(subject).to receive(:endpoints).and_return([]) @@ -329,12 +268,4 @@ class << self .to raise_error(ArgumentError, 'route_param :`foo` constrains :`foo`; pass the constraint itself, or a Hash of requirements to the enclosing namespace') end end - - describe '.versions' do - it 'returns last defined version' do - subject.version 'v1' - subject.version 'v2' - expect(subject.version).to eq('v2') - end - end end diff --git a/spec/grape/dsl/versioning_spec.rb b/spec/grape/dsl/versioning_spec.rb new file mode 100644 index 000000000..e3437e9d2 --- /dev/null +++ b/spec/grape/dsl/versioning_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe Grape::DSL::Versioning do + subject { dummy_class } + + let(:dummy_class) do + Class.new do + extend Grape::DSL::Versioning + extend Grape::DSL::Settings + end + end + + describe '.version' do + it 'sets a version for route' do + version = 'v1' + expect(subject.version(version)).to eq(version) + expect(subject.inheritable_setting.version).to eq([version]) + expect(subject.inheritable_setting.version_options).to eq(Grape::DSL::VersionOptions.new) + end + end + + describe '.versions' do + it 'returns last defined version' do + subject.version 'v1' + subject.version 'v2' + expect(subject.version).to eq('v2') + end + end +end