Skip to content

Commit 66a7d51

Browse files
committed
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
1 parent 54c67e1 commit 66a7d51

File tree

2 files changed

+172
-4
lines changed

2 files changed

+172
-4
lines changed

lib/grape-swagger/token_owner_resolver.rb

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@ module GrapeSwagger
44
class TokenOwnerResolver
55
class << self
66
SUPPORTED_ARITY_TYPES = %i[req opt rest keyreq key keyrest].freeze
7+
UNRESOLVED = Object.new.freeze
8+
private_constant :UNRESOLVED
79

810
def resolve(endpoint, method_name)
911
return if method_name.nil?
1012

1113
method_name = method_name.to_sym
12-
unless endpoint.respond_to?(method_name, true)
13-
raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}"
14-
end
14+
return endpoint.public_send(method_name) if endpoint.respond_to?(method_name, true)
15+
16+
helper_value = resolve_from_helpers(endpoint, method_name)
17+
return helper_value unless helper_value.equal?(UNRESOLVED)
1518

16-
endpoint.public_send(method_name)
19+
raise NoMethodError, "undefined method `#{method_name}` for #{endpoint.inspect}"
1720
end
1821

1922
def evaluate_proc(callable, token_owner)
@@ -29,6 +32,88 @@ def accepts_argument?(callable)
2932

3033
callable.parameters.any? { |type, _| SUPPORTED_ARITY_TYPES.include?(type) }
3134
end
35+
36+
def resolve_from_helpers(endpoint, method_name)
37+
helpers = gather_helpers(endpoint)
38+
return UNRESOLVED if helpers.empty?
39+
40+
helpers.each do |helper|
41+
resolved = resolve_from_helper(endpoint, helper, method_name)
42+
return resolved unless resolved.equal?(UNRESOLVED)
43+
end
44+
45+
UNRESOLVED
46+
end
47+
48+
def gather_helpers(endpoint)
49+
return [] if endpoint.nil?
50+
51+
helpers = []
52+
endpoint_helpers = fetch_endpoint_helpers(endpoint)
53+
helpers.concat(normalize_helpers(endpoint_helpers)) if endpoint_helpers
54+
55+
stackable_helpers = fetch_stackable_helpers(endpoint)
56+
helpers.concat(normalize_helpers(stackable_helpers)) if stackable_helpers
57+
58+
helpers.compact.uniq
59+
end
60+
61+
def resolve_from_helper(endpoint, helper, method_name)
62+
if helper.is_a?(Module)
63+
return UNRESOLVED unless helper_method_defined?(helper, method_name)
64+
65+
return helper.instance_method(method_name).bind(endpoint).call
66+
end
67+
68+
helper.respond_to?(method_name, true) ? helper.public_send(method_name) : UNRESOLVED
69+
rescue NameError
70+
UNRESOLVED
71+
end
72+
73+
def helper_method_defined?(helper, method_name)
74+
helper.method_defined?(method_name) || helper.private_method_defined?(method_name)
75+
end
76+
77+
def normalize_helpers(helpers)
78+
case helpers
79+
when nil, false
80+
[]
81+
when Module
82+
[helpers]
83+
when Array
84+
helpers.compact
85+
else
86+
if helpers.respond_to?(:key?) && helpers.respond_to?(:[]) && helpers.key?(:helpers)
87+
normalize_helpers(helpers[:helpers])
88+
elsif helpers.respond_to?(:to_a)
89+
Array(helpers.to_a).flatten.compact
90+
else
91+
Array(helpers).compact
92+
end
93+
end
94+
end
95+
96+
def fetch_endpoint_helpers(endpoint)
97+
return unless endpoint.respond_to?(:helpers, true)
98+
99+
endpoint.__send__(:helpers)
100+
rescue StandardError
101+
nil
102+
end
103+
104+
def fetch_stackable_helpers(endpoint)
105+
return unless endpoint.respond_to?(:inheritable_setting, true)
106+
107+
setting = endpoint.__send__(:inheritable_setting)
108+
return unless setting.respond_to?(:namespace_stackable)
109+
110+
namespace_stackable = setting.namespace_stackable
111+
return unless namespace_stackable.respond_to?(:[])
112+
113+
namespace_stackable[:helpers]
114+
rescue StandardError
115+
nil
116+
end
32117
end
33118
end
34119
end
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe GrapeSwagger::TokenOwnerResolver do
6+
describe '.resolve' do
7+
let(:helper_module) do
8+
Module.new do
9+
def current_user_id
10+
'user-123'
11+
end
12+
end
13+
end
14+
15+
let(:api_class) do
16+
mod = helper_module
17+
Class.new(Grape::API) do
18+
helpers mod
19+
20+
helpers do
21+
def token_owner
22+
{ id: 7, email: '[email protected]' }
23+
end
24+
end
25+
26+
get('/status') { { status: 'ok' } }
27+
end
28+
end
29+
30+
before { api_class.compile! }
31+
32+
let(:endpoint) { api_class.endpoints.first }
33+
34+
it 'returns nil when no method name is provided' do
35+
expect(described_class.resolve(endpoint, nil)).to be_nil
36+
end
37+
38+
it 'returns the resolved value when method exists' do
39+
expect(described_class.resolve(endpoint, :token_owner)).to eq(id: 7, email: '[email protected]')
40+
end
41+
42+
it 'raises when the endpoint does not respond to the method' do
43+
expect do
44+
expect(described_class.resolve(endpoint, :unknown))
45+
end.to raise_error(NoMethodError, /undefined method `unknown`/)
46+
end
47+
48+
context 'when helpers are included from a module' do
49+
it 'resolves the owner using the helper module from the namespace stack' do
50+
expect(described_class.resolve(endpoint, :current_user_id)).to eq('user-123')
51+
end
52+
end
53+
end
54+
55+
describe '.evaluate_proc' do
56+
let(:token_owner) { double(:token_owner) }
57+
58+
it 'executes callables without arguments directly' do
59+
callable = -> { :owner }
60+
61+
expect(callable).to receive(:call).with(no_args).and_call_original
62+
expect(described_class.evaluate_proc(callable, token_owner)).to eq(:owner)
63+
end
64+
65+
it 'passes the token owner when the callable accepts an argument' do
66+
callable = ->(owner) { owner }
67+
68+
allow(callable).to receive(:call).with(token_owner).and_call_original
69+
expect(described_class.evaluate_proc(callable, token_owner)).to eq(token_owner)
70+
expect(callable).to have_received(:call).with(token_owner)
71+
end
72+
73+
it 'defaults to calling without arguments when arity cannot be detected' do
74+
callable = Class.new do
75+
def call(owner = :undetected)
76+
owner
77+
end
78+
end.new
79+
80+
expect(described_class.evaluate_proc(callable, token_owner)).to eq(:undetected)
81+
end
82+
end
83+
end

0 commit comments

Comments
 (0)