From 2bbbf71b96bf2b7e1ed1c6f3858c52c9c842c748 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sat, 25 Oct 2025 01:19:06 +0200 Subject: [PATCH 1/9] Support Grape 3 --- Gemfile | 4 +-- grape-swagger.gemspec | 2 +- lib/grape-swagger.rb | 6 ++-- lib/grape-swagger/endpoint.rb | 6 +++- .../request_param_parsers/body.rb | 15 ++++---- lib/grape-swagger/token_owner_resolver.rb | 34 +++++++++++++++++++ 6 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 lib/grape-swagger/token_owner_resolver.rb diff --git a/Gemfile b/Gemfile index 66255fc2..e9aa020a 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' gemspec # gem 'grape', git: 'https://github.com/ruby-grape/grape' -gem 'grape', case version = ENV.fetch('GRAPE_VERSION', '< 3.0') +gem 'grape', case version = ENV.fetch('GRAPE_VERSION', '< 4.0') when 'HEAD' { git: 'https://github.com/ruby-grape/grape' } else @@ -20,7 +20,7 @@ group :development, :test do gem 'pry', platforms: [:mri] gem 'pry-byebug', platforms: [:mri] - grape_version = ENV.fetch('GRAPE_VERSION', '2.2.0') + grape_version = ENV.fetch('GRAPE_VERSION', '2.4.0') if grape_version == 'HEAD' || Gem::Version.new(grape_version) >= Gem::Version.new('2.0.0') gem 'rack', '>= 3.0' else diff --git a/grape-swagger.gemspec b/grape-swagger.gemspec index 2ced24ac..d88943ef 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', '< 3.0' + s.add_dependency 'grape', '>= 1.7', '< 4.0' s.files = Dir['lib/**/*', '*.md', 'LICENSE.txt', 'grape-swagger.gemspec'] s.require_paths = ['lib'] diff --git a/lib/grape-swagger.rb b/lib/grape-swagger.rb index 8eb42934..9e929b25 100644 --- a/lib/grape-swagger.rb +++ b/lib/grape-swagger.rb @@ -11,6 +11,7 @@ require 'grape-swagger/doc_methods' require 'grape-swagger/model_parsers' require 'grape-swagger/request_param_parser_registry' +require 'grape-swagger/token_owner_resolver' module GrapeSwagger class << self @@ -184,12 +185,13 @@ def combine_namespaces(app) endpoint = endpoints.shift endpoints.push(*endpoint.options[:app].endpoints) if endpoint.options[:app] - ns = endpoint.namespace_stackable(:namespace).last + namespace_stackable = endpoint.inheritable_setting.namespace_stackable + ns = (namespace_stackable[:namespace] || []).last next unless ns # use the full namespace here (not the latest level only) # and strip leading slash - mount_path = (endpoint.namespace_stackable(:mount_path) || []).join('/') + mount_path = (namespace_stackable[:mount_path] || []).join('/') full_namespace = (mount_path + endpoint.namespace).sub(/\/{2,}/, '/').sub(/^\//, '') combined_namespaces[full_namespace] = ns end diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index 54dd8191..2c7be5ed 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -5,6 +5,7 @@ require_relative 'request_param_parsers/headers' require_relative 'request_param_parsers/route' require_relative 'request_param_parsers/body' +require_relative 'token_owner_resolver' module Grape class Endpoint # rubocop:disable Metrics/ClassLength @@ -439,7 +440,10 @@ def hidden?(route, options) route_hidden = route.options[:hidden] if route.options.key?(:hidden) return route_hidden unless route_hidden.is_a?(Proc) - options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call + return route_hidden.call unless options[:token_owner] + + token_owner = GrapeSwagger::TokenOwnerResolver.resolve(self, options[:token_owner]) + GrapeSwagger::TokenOwnerResolver.evaluate_proc(route_hidden, token_owner) end def hidden_parameter?(value) diff --git a/lib/grape-swagger/request_param_parsers/body.rb b/lib/grape-swagger/request_param_parsers/body.rb index b8814396..6feeca6f 100644 --- a/lib/grape-swagger/request_param_parsers/body.rb +++ b/lib/grape-swagger/request_param_parsers/body.rb @@ -54,13 +54,7 @@ def public_parameter?(param_options) return true unless param_options.key?(:documentation) && !param_options[:required] param_hidden = param_options[:documentation].fetch(:hidden, false) - if param_hidden.is_a?(Proc) - param_hidden = if settings[:token_owner] - param_hidden.call(endpoint.send(settings[:token_owner].to_sym)) - else - param_hidden.call - end - end + param_hidden = evaluate_hidden_proc(param_hidden) if param_hidden.is_a?(Proc) !param_hidden end @@ -69,6 +63,13 @@ def includes_body_param? options.dig(:documentation, :param_type) == 'body' || options.dig(:documentation, :in) == 'body' end end + + def evaluate_hidden_proc(hidden_proc) + return hidden_proc.call unless settings[:token_owner] + + token_owner = GrapeSwagger::TokenOwnerResolver.resolve(endpoint, settings[:token_owner]) + GrapeSwagger::TokenOwnerResolver.evaluate_proc(hidden_proc, token_owner) + end end end end diff --git a/lib/grape-swagger/token_owner_resolver.rb b/lib/grape-swagger/token_owner_resolver.rb new file mode 100644 index 00000000..30c3efeb --- /dev/null +++ b/lib/grape-swagger/token_owner_resolver.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module GrapeSwagger + class TokenOwnerResolver + class << self + SUPPORTED_ARITY_TYPES = %i[req opt rest keyreq key keyrest].freeze + + def resolve(endpoint, method_name) + return if method_name.nil? + + method_name = method_name.to_sym + unless endpoint.respond_to?(method_name, true) + raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}" + end + + endpoint.public_send(method_name) + end + + def evaluate_proc(callable, token_owner) + return callable.call unless accepts_argument?(callable) + + callable.call(token_owner) + end + + private + + def accepts_argument?(callable) + return false unless callable.respond_to?(:parameters) + + callable.parameters.any? { |type, _| SUPPORTED_ARITY_TYPES.include?(type) } + end + end + end +end From b14469df910adf8f4fa652c2436fa197a59c411a Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 02:52:39 +0200 Subject: [PATCH 2/9] Update compatibility matrix --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f429dd9..a9908100 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ The following versions of grape, grape-entity and grape-swagger can currently be | >= 1.0.0 | 2.0 | >= 1.3.0 | >= 0.5.0 | >= 2.4.1 | | >= 2.0.0 | 2.0 | >= 1.7.0 | >= 0.5.0 | >= 2.4.1 | | >= 2.0.0 ... < 2.2 | 2.0 | >= 1.8.0 ... < 2.3.0 | >= 0.5.0 | >= 2.4.1 | +| > 2.1.2 ... < 2.2 | 2.0 | >= 1.8.0 ... < 3.0.0 | >= 0.5.0 | >= 2.4.1 | ## Swagger-Spec From ea166f3a294e1f618a6db6851511dc275e41ce71 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 02:52:56 +0200 Subject: [PATCH 3/9] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa559f7e..b75eef0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ #### Fixes +* [#966](https://github.com/ruby-grape/grape-swagger/pull/966): Grape 3.0 compatibility - [@numbata](https://github.com/numbata). * [#954](https://github.com/ruby-grape/grape-swagger/pull/954): Ruby 3.5 compatibility: add cgi gem, drop runtime `rack‑test` - [@numbata](https://github.com/numbata). * [#948](https://github.com/ruby-grape/grape-swagger/pull/948): Grape 2.3.0 and Ruby 3.5 compatibility - [@numbata](https://github.com/numbata). * Your contribution here. From 54c67e14e6c7f798e4325ec8f5331fc085a464ff Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 02:21:01 +0100 Subject: [PATCH 4/9] Bump CI matrix --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea303930..c528635f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,9 @@ jobs: - { ruby: '3.3', grape: '2.2.0' } - { ruby: '3.4', grape: '2.2.0' } - { ruby: 'head', grape: '2.2.0' } - # - { ruby: '3.1', grape: 'HEAD' } - # - { ruby: '3.2', grape: 'HEAD' } - # - { ruby: '3.3', grape: 'HEAD' } - # - { ruby: '3.4', grape: 'HEAD' } + - { ruby: '3.2', grape: 'HEAD' } + - { ruby: '3.3', grape: 'HEAD' } + - { ruby: '3.4', grape: 'HEAD' } name: test (ruby=${{ matrix.entry.ruby }}, grape=${{ matrix.entry.grape }}) runs-on: ubuntu-latest needs: ['rubocop'] From 66a7d5138e344d4a10308cd9f83a37bec745166a Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 12:13:24 +0100 Subject: [PATCH 5/9] Enhance TokenOwnerResolver for Grape 3.0 compatibility - Implement robust token owner resolution from endpoint methods, helper modules, and stackable helpers - Add support for detecting and properly evaluating Proc arguments via parameter inspection - Add comprehensive test coverage for resolver behavior including: * Direct method resolution on endpoints * Helper module resolution via namespace stack * Proc evaluation with and without arguments * Error handling for undefined methods --- lib/grape-swagger/token_owner_resolver.rb | 93 ++++++++++++++++++++++- spec/lib/token_owner_resolver_spec.rb | 83 ++++++++++++++++++++ 2 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 spec/lib/token_owner_resolver_spec.rb diff --git a/lib/grape-swagger/token_owner_resolver.rb b/lib/grape-swagger/token_owner_resolver.rb index 30c3efeb..138fe242 100644 --- a/lib/grape-swagger/token_owner_resolver.rb +++ b/lib/grape-swagger/token_owner_resolver.rb @@ -4,16 +4,19 @@ module GrapeSwagger class TokenOwnerResolver class << self SUPPORTED_ARITY_TYPES = %i[req opt rest keyreq key keyrest].freeze + UNRESOLVED = Object.new.freeze + private_constant :UNRESOLVED def resolve(endpoint, method_name) return if method_name.nil? method_name = method_name.to_sym - unless endpoint.respond_to?(method_name, true) - raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}" - end + return endpoint.public_send(method_name) if endpoint.respond_to?(method_name, true) + + helper_value = resolve_from_helpers(endpoint, method_name) + return helper_value unless helper_value.equal?(UNRESOLVED) - endpoint.public_send(method_name) + raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}" end def evaluate_proc(callable, token_owner) @@ -29,6 +32,88 @@ def accepts_argument?(callable) callable.parameters.any? { |type, _| SUPPORTED_ARITY_TYPES.include?(type) } end + + def resolve_from_helpers(endpoint, method_name) + helpers = gather_helpers(endpoint) + return UNRESOLVED if helpers.empty? + + helpers.each do |helper| + resolved = resolve_from_helper(endpoint, helper, method_name) + return resolved unless resolved.equal?(UNRESOLVED) + end + + UNRESOLVED + end + + def gather_helpers(endpoint) + return [] if endpoint.nil? + + helpers = [] + endpoint_helpers = fetch_endpoint_helpers(endpoint) + helpers.concat(normalize_helpers(endpoint_helpers)) if endpoint_helpers + + stackable_helpers = fetch_stackable_helpers(endpoint) + helpers.concat(normalize_helpers(stackable_helpers)) if stackable_helpers + + helpers.compact.uniq + end + + def resolve_from_helper(endpoint, helper, method_name) + if helper.is_a?(Module) + return UNRESOLVED unless helper_method_defined?(helper, method_name) + + return helper.instance_method(method_name).bind(endpoint).call + end + + helper.respond_to?(method_name, true) ? helper.public_send(method_name) : UNRESOLVED + rescue NameError + UNRESOLVED + end + + def helper_method_defined?(helper, method_name) + helper.method_defined?(method_name) || helper.private_method_defined?(method_name) + end + + def normalize_helpers(helpers) + case helpers + when nil, false + [] + when Module + [helpers] + when Array + helpers.compact + else + if helpers.respond_to?(:key?) && helpers.respond_to?(:[]) && helpers.key?(:helpers) + normalize_helpers(helpers[:helpers]) + elsif helpers.respond_to?(:to_a) + Array(helpers.to_a).flatten.compact + else + Array(helpers).compact + end + end + end + + def fetch_endpoint_helpers(endpoint) + return unless endpoint.respond_to?(:helpers, true) + + endpoint.__send__(:helpers) + rescue StandardError + nil + end + + def fetch_stackable_helpers(endpoint) + return unless endpoint.respond_to?(:inheritable_setting, true) + + setting = endpoint.__send__(:inheritable_setting) + return unless setting.respond_to?(:namespace_stackable) + + namespace_stackable = setting.namespace_stackable + return unless namespace_stackable.respond_to?(:[]) + + namespace_stackable[:helpers] + rescue StandardError + nil + end end end end diff --git a/spec/lib/token_owner_resolver_spec.rb b/spec/lib/token_owner_resolver_spec.rb new file mode 100644 index 00000000..e0ecb0af --- /dev/null +++ b/spec/lib/token_owner_resolver_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GrapeSwagger::TokenOwnerResolver do + describe '.resolve' do + let(:helper_module) do + Module.new do + def current_user_id + 'user-123' + end + end + end + + let(:api_class) do + mod = helper_module + Class.new(Grape::API) do + helpers mod + + helpers do + def token_owner + { id: 7, email: 'owner@example.com' } + end + end + + get('/status') { { status: 'ok' } } + end + end + + before { api_class.compile! } + + let(:endpoint) { api_class.endpoints.first } + + it 'returns nil when no method name is provided' do + expect(described_class.resolve(endpoint, nil)).to be_nil + end + + it 'returns the resolved value when method exists' do + expect(described_class.resolve(endpoint, :token_owner)).to eq(id: 7, email: 'owner@example.com') + end + + it 'raises when the endpoint does not respond to the method' do + expect do + expect(described_class.resolve(endpoint, :unknown)) + end.to raise_error(NoMethodError, /undefined method `unknown`/) + end + + context 'when helpers are included from a module' do + it 'resolves the owner using the helper module from the namespace stack' do + expect(described_class.resolve(endpoint, :current_user_id)).to eq('user-123') + end + end + end + + describe '.evaluate_proc' do + let(:token_owner) { double(:token_owner) } + + it 'executes callables without arguments directly' do + callable = -> { :owner } + + expect(callable).to receive(:call).with(no_args).and_call_original + expect(described_class.evaluate_proc(callable, token_owner)).to eq(:owner) + end + + it 'passes the token owner when the callable accepts an argument' do + callable = ->(owner) { owner } + + allow(callable).to receive(:call).with(token_owner).and_call_original + expect(described_class.evaluate_proc(callable, token_owner)).to eq(token_owner) + expect(callable).to have_received(:call).with(token_owner) + end + + it 'defaults to calling without arguments when arity cannot be detected' do + callable = Class.new do + def call(owner = :undetected) + owner + end + end.new + + expect(described_class.evaluate_proc(callable, token_owner)).to eq(:undetected) + end + end +end From 1e002a36d859c12805b391ac9d6745e3eb4055e9 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 19:51:20 +0100 Subject: [PATCH 6/9] refactor: Clean up and simplify TokenOwnerResolver implementation Remove dead code and improve error handling: - Remove fetch_endpoint_helpers: Endpoints don't have a public helpers method, making this code path unreachable - Simplify resolve_from_helper: Remove non-Module branch since helpers from namespace_stackable are always Modules - Simplify gather_helpers: Only one source of helpers (stackable), remove unnecessary array consolidation - Remove __send__ usage: inheritable_setting is a public method, use direct call - Improve exception handling: Replace broad rescue StandardError with specific NoMethodError and NameError catches in fetch_stackable_helpers Error handling improvements: - Add GrapeSwagger::Errors::TokenOwnerNotFound inheriting from NoMethodError for more semantic error hierarchy - Update resolver to raise custom error instead of generic NoMethodError, allowing users to catch grape-swagger-specific issues --- lib/grape-swagger/errors.rb | 2 + lib/grape-swagger/token_owner_resolver.rb | 62 ++++++++--------------- spec/lib/token_owner_resolver_spec.rb | 57 ++++++++++++++++++++- 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/lib/grape-swagger/errors.rb b/lib/grape-swagger/errors.rb index b4781699..d3cd683d 100644 --- a/lib/grape-swagger/errors.rb +++ b/lib/grape-swagger/errors.rb @@ -13,5 +13,7 @@ def tell!(what) end end end + + class TokenOwnerNotFound < NoMethodError; end end end diff --git a/lib/grape-swagger/token_owner_resolver.rb b/lib/grape-swagger/token_owner_resolver.rb index 138fe242..2b59a699 100644 --- a/lib/grape-swagger/token_owner_resolver.rb +++ b/lib/grape-swagger/token_owner_resolver.rb @@ -16,7 +16,7 @@ def resolve(endpoint, method_name) helper_value = resolve_from_helpers(endpoint, method_name) return helper_value unless helper_value.equal?(UNRESOLVED) - raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}" + raise Errors::TokenOwnerNotFound, "undefined method `#{method_name}` for #{endpoint.inspect}" end def evaluate_proc(callable, token_owner) @@ -27,12 +27,6 @@ def evaluate_proc(callable, token_owner) private - def accepts_argument?(callable) - return false unless callable.respond_to?(:parameters) - - callable.parameters.any? { |type, _| SUPPORTED_ARITY_TYPES.include?(type) } - end - def resolve_from_helpers(endpoint, method_name) helpers = gather_helpers(endpoint) return UNRESOLVED if helpers.empty? @@ -48,30 +42,22 @@ def resolve_from_helpers(endpoint, method_name) def gather_helpers(endpoint) return [] if endpoint.nil? - helpers = [] - endpoint_helpers = fetch_endpoint_helpers(endpoint) - helpers.concat(normalize_helpers(endpoint_helpers)) if endpoint_helpers - stackable_helpers = fetch_stackable_helpers(endpoint) - helpers.concat(normalize_helpers(stackable_helpers)) if stackable_helpers - - helpers.compact.uniq + normalize_helpers(stackable_helpers) end - def resolve_from_helper(endpoint, helper, method_name) - if helper.is_a?(Module) - return UNRESOLVED unless helper_method_defined?(helper, method_name) + def fetch_stackable_helpers(endpoint) + return unless endpoint.respond_to?(:inheritable_setting) - return helper.instance_method(method_name).bind(endpoint).call - end + setting = endpoint.inheritable_setting + return unless setting.respond_to?(:namespace_stackable) - helper.respond_to?(method_name, true) ? helper.public_send(method_name) : UNRESOLVED - rescue NameError - UNRESOLVED - end + namespace_stackable = setting.namespace_stackable + return unless namespace_stackable.respond_to?(:[]) - def helper_method_defined?(helper, method_name) - helper.method_defined?(method_name) || helper.private_method_defined?(method_name) + namespace_stackable[:helpers] + rescue NameError + nil end def normalize_helpers(helpers) @@ -93,26 +79,22 @@ def normalize_helpers(helpers) end end - def fetch_endpoint_helpers(endpoint) - return unless endpoint.respond_to?(:helpers, true) + def resolve_from_helper(endpoint, helper, method_name) + return UNRESOLVED unless helper_method_defined?(helper, method_name) - endpoint.__send__(:helpers) - rescue StandardError - nil + helper.instance_method(method_name).bind(endpoint).call + rescue NameError + UNRESOLVED end - def fetch_stackable_helpers(endpoint) - return unless endpoint.respond_to?(:inheritable_setting, true) - - setting = endpoint.__send__(:inheritable_setting) - return unless setting.respond_to?(:namespace_stackable) + def helper_method_defined?(helper, method_name) + helper.method_defined?(method_name) || helper.private_method_defined?(method_name) + end - namespace_stackable = setting.namespace_stackable - return unless namespace_stackable.respond_to?(:[]) + def accepts_argument?(callable) + return false unless callable.respond_to?(:parameters) - namespace_stackable[:helpers] - rescue StandardError - nil + callable.parameters.any? { |type, _| SUPPORTED_ARITY_TYPES.include?(type) } end end end diff --git a/spec/lib/token_owner_resolver_spec.rb b/spec/lib/token_owner_resolver_spec.rb index e0ecb0af..1481bab8 100644 --- a/spec/lib/token_owner_resolver_spec.rb +++ b/spec/lib/token_owner_resolver_spec.rb @@ -42,7 +42,7 @@ def token_owner it 'raises when the endpoint does not respond to the method' do expect do expect(described_class.resolve(endpoint, :unknown)) - end.to raise_error(NoMethodError, /undefined method `unknown`/) + end.to raise_error(GrapeSwagger::Errors::TokenOwnerNotFound, /undefined method `unknown`/) end context 'when helpers are included from a module' do @@ -80,4 +80,59 @@ def call(owner = :undetected) expect(described_class.evaluate_proc(callable, token_owner)).to eq(:undetected) end end + + describe '.resolve_from_helper' do + let(:helper_module) do + Module.new do + def helper_method + 'helper_result' + end + end + end + + let(:endpoint) { instance_double(Grape::Endpoint) } + + it 'resolves the method from the Module helper' do + result = described_class.send(:resolve_from_helper, endpoint, helper_module, :helper_method) + expect(result).to eq('helper_result') + end + + it 'returns a frozen sentinel object when method does not exist on the Module' do + result = described_class.send(:resolve_from_helper, endpoint, helper_module, :nonexistent) + # UNRESOLVED is a private constant, so we check by type and behavior + expect(result).to be_a(Object) + expect(result).to be_frozen + # Verify it's the same object on repeated calls (singleton pattern) + result2 = described_class.send(:resolve_from_helper, endpoint, helper_module, :nonexistent) + expect(result).to equal(result2) + end + end + + describe 'endpoint helpers access' do + let(:helper_module) do + Module.new do + def current_user + { id: 42, name: 'Test User' } + end + end + end + + let(:api_class) do + mod = helper_module + Class.new(Grape::API) do + helpers mod + + get('/test') { { ok: true } } + end + end + + before { api_class.compile! } + + let(:endpoint) { api_class.endpoints.first } + + it 'resolves helper methods from namespace stack' do + resolved_value = described_class.resolve(endpoint, :current_user) + expect(resolved_value).to eq(id: 42, name: 'Test User') + end + end end From c0d2b72e9fd13227c4342d4b5d44f892e26811d9 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 20:40:55 +0100 Subject: [PATCH 7/9] chore: Constrain activesupport < 7.2 for Grape 1.8.0 compatibility Grape 1.8.0 is incompatible with activesupport >= 7.2 due to deprecation API changes. Add conditional constraint to allow testing with Grape 1.8.0 while maintaining compatibility with Grape 2.x and 3.x. --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index e9aa020a..e0d4802a 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ group :development, :test do if grape_version == 'HEAD' || Gem::Version.new(grape_version) >= Gem::Version.new('2.0.0') gem 'rack', '>= 3.0' else + gem 'activesupport', '< 7.2' gem 'rack', '< 3.0' end From cb72b85c49a9e8dca37f8009643a804e048655e8 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 20:54:43 +0100 Subject: [PATCH 8/9] fix: Use endpoint.class instead of endpoint.inspect in error message Some Grape versions have protected inspect methods on endpoint objects. Use endpoint.class for the error message instead, which is simpler and works reliably across all Grape versions (1.8.0, 2.1.3, 2.4.0, 3.0+). This resolves failures when testing with Ruby 3.1.0 and Grape 2.1.3. --- lib/grape-swagger/token_owner_resolver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grape-swagger/token_owner_resolver.rb b/lib/grape-swagger/token_owner_resolver.rb index 2b59a699..9bcc794c 100644 --- a/lib/grape-swagger/token_owner_resolver.rb +++ b/lib/grape-swagger/token_owner_resolver.rb @@ -16,7 +16,7 @@ def resolve(endpoint, method_name) helper_value = resolve_from_helpers(endpoint, method_name) return helper_value unless helper_value.equal?(UNRESOLVED) - raise Errors::TokenOwnerNotFound, "undefined method `#{method_name}` for #{endpoint.inspect}" + raise Errors::TokenOwnerNotFound, "undefined method `#{method_name}` for #{endpoint.class}" end def evaluate_proc(callable, token_owner) From 173ee0f49f3c614313f4a175af211f0c3f1f68f4 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Sun, 26 Oct 2025 21:21:30 +0100 Subject: [PATCH 9/9] Align README.md file with gemspec-file --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a9908100..e83fd48a 100644 --- a/README.md +++ b/README.md @@ -113,19 +113,19 @@ This screenshot is based on the [Hussars](https://github.com/LeFnord/hussars) sa The following versions of grape, grape-entity and grape-swagger can currently be used together. -| grape-swagger | swagger spec | grape | grape-entity | representable | -| ------------------ | ------------ | ----------------------- | ------------ | ------------- | -| 0.10.5 | 1.2 | >= 0.10.0 ... <= 0.14.0 | < 0.5.0 | n/a | -| 0.11.0 | 1.2 | >= 0.16.2 | < 0.5.0 | n/a | -| 0.25.2 | 2.0 | >= 0.14.0 ... <= 0.18.0 | <= 0.6.0 | >= 2.4.1 | -| 0.26.0 | 2.0 | >= 0.16.2 ... <= 1.1.0 | <= 0.6.1 | >= 2.4.1 | -| 0.27.0 | 2.0 | >= 0.16.2 ... <= 1.1.0 | >= 0.5.0 | >= 2.4.1 | -| 0.32.0 | 2.0 | >= 0.16.2 | >= 0.5.0 | >= 2.4.1 | -| 0.34.0 | 2.0 | >= 0.16.2 ... < 1.3.0 | >= 0.5.0 | >= 2.4.1 | -| >= 1.0.0 | 2.0 | >= 1.3.0 | >= 0.5.0 | >= 2.4.1 | -| >= 2.0.0 | 2.0 | >= 1.7.0 | >= 0.5.0 | >= 2.4.1 | -| >= 2.0.0 ... < 2.2 | 2.0 | >= 1.8.0 ... < 2.3.0 | >= 0.5.0 | >= 2.4.1 | -| > 2.1.2 ... < 2.2 | 2.0 | >= 1.8.0 ... < 3.0.0 | >= 0.5.0 | >= 2.4.1 | +| grape-swagger | swagger spec | grape | grape-entity | representable | +| --------------------- | ------------ | ----------------------- | ------------ | ------------- | +| 0.10.5 | 1.2 | >= 0.10.0 ... <= 0.14.0 | < 0.5.0 | n/a | +| 0.11.0 | 1.2 | >= 0.16.2 | < 0.5.0 | n/a | +| 0.25.2 | 2.0 | >= 0.14.0 ... <= 0.18.0 | <= 0.6.0 | >= 2.4.1 | +| 0.26.0 | 2.0 | >= 0.16.2 ... <= 1.1.0 | <= 0.6.1 | >= 2.4.1 | +| 0.27.0 | 2.0 | >= 0.16.2 ... <= 1.1.0 | >= 0.5.0 | >= 2.4.1 | +| 0.32.0 | 2.0 | >= 0.16.2 | >= 0.5.0 | >= 2.4.1 | +| 0.34.0 | 2.0 | >= 0.16.2 ... < 1.3.0 | >= 0.5.0 | >= 2.4.1 | +| >= 1.0.0 | 2.0 | >= 1.3.0 | >= 0.5.0 | >= 2.4.1 | +| >= 2.0.0 | 2.0 | >= 1.7.0 | >= 0.5.0 | >= 2.4.1 | +| >= 2.0.0 ... <= 2.1.2 | 2.0 | >= 1.8.0 ... < 2.3.0 | >= 0.5.0 | >= 2.4.1 | +| > 2.1.2 | 2.0 | >= 1.8.0 ... < 4.0 | >= 0.5.0 | >= 2.4.1 | ## Swagger-Spec