From 206cf3430b7d411541b61cac6ff55640c532d0b8 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Sun, 17 May 2026 10:38:27 +0200 Subject: [PATCH 1/6] Fix Grape 3.2.1 compatibility: keyword args for desc and custom types Grape 3.2.1 deprecates passing a positional options Hash to `desc` and requires unknown types in params blocks to either be a registered dry-type or implement `parse` (custom type protocol). This change addresses both. - Pass keyword arguments to `desc` in doc_methods.rb so Grape's deprecator does not fire (or raise when behavior = :raise) - Add `self.parse` to the mock `Entities::NestedModule::ApiResponse` so Grape treats it as a custom type instead of attempting a dry-types lookup - Move `type: 'Object'` into the `documentation` hash in params_example_spec so Grape no longer sees an unknown string type; grape-swagger still reads it from the merged settings and emits the expected swagger output --- CHANGELOG.md | 2 +- lib/grape-swagger/doc_methods.rb | 4 ++-- spec/support/model_parsers/mock_parser.rb | 4 +++- spec/swagger_v2/params_example_spec.rb | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428ddb86e..004cc8ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ #### Fixes -* Your contribution here. +* [#977](https://github.com/ruby-grape/grape-swagger/issues/977): Pass keyword arguments to `desc` to fix deprecation warning from Grape - [@numbata](https://github.com/numbata). ### 2.1.4 (2026-02-02) diff --git a/lib/grape-swagger/doc_methods.rb b/lib/grape-swagger/doc_methods.rb index 9e22499d7..dd727a825 100644 --- a/lib/grape-swagger/doc_methods.rb +++ b/lib/grape-swagger/doc_methods.rb @@ -93,7 +93,7 @@ def setup(options) setup_formatter(options[:format]) - desc api_doc.delete(:desc), api_doc + desc api_doc.delete(:desc), **api_doc instance_eval(guard) unless guard.nil? @@ -105,7 +105,7 @@ def setup(options) .output_path_definitions(target_class.combined_namespace_routes, self, target_class, options) end - desc specific_api_doc.delete(:desc), { params: specific_api_doc.delete(:params) || {}, **specific_api_doc } + desc specific_api_doc.delete(:desc), params: specific_api_doc.delete(:params) || {}, **specific_api_doc params do requires :name, type: String, desc: 'Resource name of mounted API' diff --git a/spec/support/model_parsers/mock_parser.rb b/spec/support/model_parsers/mock_parser.rb index 13bdcc04c..a81d1728e 100644 --- a/spec/support/model_parsers/mock_parser.rb +++ b/spec/support/model_parsers/mock_parser.rb @@ -72,7 +72,9 @@ class RecursiveModel < OpenStruct; end class DocumentedHashAndArrayModel < OpenStruct; end module NestedModule - class ApiResponse < OpenStruct; end + class ApiResponse < OpenStruct + def self.parse(val) = val + end end end end diff --git a/spec/swagger_v2/params_example_spec.rb b/spec/swagger_v2/params_example_spec.rb index fbddf7cca..ebee8efe9 100644 --- a/spec/swagger_v2/params_example_spec.rb +++ b/spec/swagger_v2/params_example_spec.rb @@ -11,7 +11,7 @@ def app params :common_params do requires :id, type: Integer, documentation: { example: 123 } optional :name, type: String, documentation: { example: 'Person' } - optional :obj, type: 'Object', documentation: { example: { 'foo' => 'bar' } } + optional :obj, documentation: { type: 'Object', example: { 'foo' => 'bar' } } end end From a79917d5518a220580ad3f35d8f8294ad30b9008 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Sun, 17 May 2026 11:01:42 +0200 Subject: [PATCH 2/6] Suppress Style/OneClassPerFile for pre-existing top-level modules Moving SwaggerRouting and SwaggerDocumentationAdder inside the GrapeSwagger namespace would be a breaking change for dependent gems that reference these constants by their top-level names. Exclude the affected files in .rubocop_todo.yml to keep CI green until that rename ships separately. --- .rubocop_todo.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 811dd12a2..587c88dc3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,6 +6,14 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 4 +# Configuration parameters: Severity. +Style/OneClassPerFile: + Exclude: + - 'lib/grape-swagger.rb' + - 'spec/support/empty_model_parser.rb' + - 'spec/swagger_v2/guarded_endpoint_spec.rb' + # Offense count: 1 # Configuration parameters: Severity, Include. # Include: **/*.gemspec From ba1228a17511b230e6c41e4ed97d66a11ebbf9c8 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Sun, 17 May 2026 13:53:19 +0200 Subject: [PATCH 3/6] Clarify intent of Grape 3.2 compatibility fixes with inline comments Add comments explaining: - 'Object' is a swagger documentation hint, not a valid Grape coercion type; the type lives in documentation: so grape-swagger picks it up via settings merge - ApiResponse.parse is required because Grape 3.2+ validates unknown types via Types.custom? (arity-1 parse check); minimal pass-through is sufficient since tests exercise documentation generation, not request coercion --- spec/support/model_parsers/mock_parser.rb | 4 ++++ spec/swagger_v2/params_example_spec.rb | 3 +++ 2 files changed, 7 insertions(+) diff --git a/spec/support/model_parsers/mock_parser.rb b/spec/support/model_parsers/mock_parser.rb index a81d1728e..de6d24c00 100644 --- a/spec/support/model_parsers/mock_parser.rb +++ b/spec/support/model_parsers/mock_parser.rb @@ -73,6 +73,10 @@ class DocumentedHashAndArrayModel < OpenStruct; end module NestedModule class ApiResponse < OpenStruct + # Grape 3.2+ requires unknown types to implement .parse (arity 1) to pass the + # Types.custom? check and avoid a dry-types lookup that would raise ArgumentError. + # The implementation is minimal because these tests exercise swagger doc generation + # only, not actual request coercion. def self.parse(val) = val end end diff --git a/spec/swagger_v2/params_example_spec.rb b/spec/swagger_v2/params_example_spec.rb index ebee8efe9..f3504e03d 100644 --- a/spec/swagger_v2/params_example_spec.rb +++ b/spec/swagger_v2/params_example_spec.rb @@ -11,6 +11,9 @@ def app params :common_params do requires :id, type: Integer, documentation: { example: 123 } optional :name, type: String, documentation: { example: 'Person' } + # 'Object' is not a valid Grape coercion type; it is a swagger documentation-only hint. + # Grape 3.2+ rejects string type names in params blocks, so the type lives in + # documentation: where grape-swagger picks it up via ParseParams#call settings merge. optional :obj, documentation: { type: 'Object', example: { 'foo' => 'bar' } } end end From d1f07b8b993b291b4a9bb5bd9c648cba7beee71f Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Sun, 17 May 2026 14:04:03 +0200 Subject: [PATCH 4/6] Fix Grape 3.2+ compatibility for representable parser's ApiResponse Representable::Decorator does not implement .parse, so Grape 3.2+ raises ArgumentError when ApiResponse is used as a param type. Same fix as mock_parser. --- spec/support/model_parsers/representable_parser.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/support/model_parsers/representable_parser.rb b/spec/support/model_parsers/representable_parser.rb index 2102ce8aa..d56fbd4de 100644 --- a/spec/support/model_parsers/representable_parser.rb +++ b/spec/support/model_parsers/representable_parser.rb @@ -99,6 +99,11 @@ class ApiResponse < Representable::Decorator property :status, documentation: { type: String } property :error, documentation: { type: ::Entities::ApiError } + + # Grape 3.2+ requires unknown types to implement .parse (arity 1) to pass the + # Types.custom? check. Representable::Decorator does not define parse, so we add + # a minimal pass-through sufficient for documentation-generation tests. + def self.parse(val) = val end end From c7458b44b14bd512e3b7666027b568e832e1b251 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 19 May 2026 01:56:37 +0200 Subject: [PATCH 5/6] Bump minimum Grape dependency to 2.1 and trim CI matrix Grape 1.8.0 and 2.0.0 are broken with Ruby 3.3+ due to a Mustermann private-method incompatibility (named_captures), producing 275 failures on both master and this branch. Supporting these versions is no longer meaningful. Raise the gemspec lower bound from >= 1.7 to >= 2.1 to reflect what actually works, and drop the 1.8.0/2.0.0 rows from the CI matrix so the build result becomes an honest signal. --- .github/workflows/ci.yml | 8 -------- grape-swagger.gemspec | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aef71158..a6ec775c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,14 +26,6 @@ jobs: strategy: matrix: entry: - - { ruby: '3.1', grape: '1.8.0' } - - { ruby: '3.2', grape: '1.8.0' } - - { ruby: '3.3', grape: '1.8.0' } - - { ruby: '3.4', grape: '1.8.0' } - - { ruby: '3.1', grape: '2.0.0' } - - { ruby: '3.2', grape: '2.0.0' } - - { ruby: '3.3', grape: '2.0.0' } - - { ruby: '3.4', grape: '2.0.0' } - { ruby: '3.1', grape: '2.1.3' } - { ruby: '3.2', grape: '2.1.3' } - { ruby: '3.3', grape: '2.1.3' } diff --git a/grape-swagger.gemspec b/grape-swagger.gemspec index d88943efa..ce9d6b7fe 100644 --- a/grape-swagger.gemspec +++ b/grape-swagger.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.metadata['rubygems_mfa_required'] = 'true' s.required_ruby_version = '>= 3.1' - s.add_dependency 'grape', '>= 1.7', '< 4.0' + s.add_dependency 'grape', '>= 2.1', '< 4.0' s.files = Dir['lib/**/*', '*.md', 'LICENSE.txt', 'grape-swagger.gemspec'] s.require_paths = ['lib'] From 54465f4c9c61bbe74ee1545605281203e4e1612e Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Tue, 19 May 2026 02:13:30 +0200 Subject: [PATCH 6/6] Recover actual types from VariantCollectionCoercer in multi-type params In Grape >= 3.3 `type: [A, B]` is wrapped in a VariantCollectionCoercer, and Grape's documentation pipeline serialises that wrapper via #to_s before storing it in route.params, which discards the original type list. The previous behaviour exposed the coercer's #inspect string (e.g. "#") as the swagger type. The live coercer is still reachable via the matching CoerceValidator's @converter, so collect_variant_types walks namespace_stackable[:validations] and builds a name => [types] lookup. When fulfill_params sees the VariantCollectionCoercer signature in the serialised type, it substitutes the real type list and lets the existing Array handling in DataType.parse_multi_type pick the primary type, matching how older Grape versions behaved when they passed the type array through directly. --- .../request_param_parsers/route.rb | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/grape-swagger/request_param_parsers/route.rb b/lib/grape-swagger/request_param_parsers/route.rb index d3d27d20c..95dc1ffb1 100644 --- a/lib/grape-swagger/request_param_parsers/route.rb +++ b/lib/grape-swagger/request_param_parsers/route.rb @@ -19,8 +19,9 @@ def parse stackable_values = route.app&.inheritable_setting&.namespace_stackable path_params = build_path_params(stackable_values) + variant_types = collect_variant_types(stackable_values) - fulfill_params(path_params) + fulfill_params(path_params, variant_types) end private @@ -47,7 +48,33 @@ def fetch_inherited_params(stackable_values) end end - def fulfill_params(path_params) + # In Grape >= 3.3 `type: [A, B]` is wrapped in VariantCollectionCoercer and + # Grape's documentation pipeline serialises it via `#to_s`, so the original + # type list is lost in route.params. The live coercer is still reachable + # through the CoerceValidator's @converter, so we rebuild a name => types + # map and restore it in fulfill_params. + def collect_variant_types(stackable_values) + variant_types = {} + return variant_types unless defined?(Grape::Validations::Types::VariantCollectionCoercer) && + defined?(Grape::Validations::Validators::CoerceValidator) && + stackable_values.respond_to?(:[]) + + Array(stackable_values[:validations]).each do |validator| + next unless validator.is_a?(Grape::Validations::Validators::CoerceValidator) + + converter = validator.instance_variable_get(:@converter) + next unless converter.is_a?(Grape::Validations::Types::VariantCollectionCoercer) + + types = converter.instance_variable_get(:@types) + Array(validator.instance_variable_get(:@attrs)).each do |attr| + variant_types[attr.to_s] = types + end + end + + variant_types + end + + def fulfill_params(path_params, variant_types) # Merge path params options into route params route.params.each_with_object({}) do |(param, definition), accum| # The route.params hash includes both parametrized params (with a string as a key) @@ -57,10 +84,18 @@ def fulfill_params(path_params) next if param.is_a?(String) && accum.key?(key) defined_options = definition.is_a?(Hash) ? definition : {} + defined_options = restore_variant_type(defined_options, param, variant_types) value = (path_params[param] || {}).merge(defined_options) accum[key] = value.empty? ? DEFAULT_PARAM_TYPE : value end end + + def restore_variant_type(defined_options, param, variant_types) + types = variant_types[param.to_s] + return defined_options unless types + + defined_options.merge(type: types) + end end end end