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/.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 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/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'] 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/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 diff --git a/spec/support/model_parsers/mock_parser.rb b/spec/support/model_parsers/mock_parser.rb index 13bdcc04c..de6d24c00 100644 --- a/spec/support/model_parsers/mock_parser.rb +++ b/spec/support/model_parsers/mock_parser.rb @@ -72,7 +72,13 @@ class RecursiveModel < OpenStruct; end class DocumentedHashAndArrayModel < OpenStruct; end module NestedModule - class ApiResponse < OpenStruct; end + 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 end end 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 diff --git a/spec/swagger_v2/params_example_spec.rb b/spec/swagger_v2/params_example_spec.rb index fbddf7cca..f3504e03d 100644 --- a/spec/swagger_v2/params_example_spec.rb +++ b/spec/swagger_v2/params_example_spec.rb @@ -11,7 +11,10 @@ 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' } } + # '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