Skip to content

Commit 4d71e0e

Browse files
numbataChau Hong Linh
andauthored
Block arity detection (#389)
* Fix the method Grape::Entity#exec_with_object to work with Ruby 3. * Fix(Entity): Improve block arity detection for `Symbol#to_proc` Addresses `ArgumentError`s when using `&:method_name` with `expose`, a scenario particularly affected by Ruby 3.0's stricter argument handling for procs. The previous arity check, including the condition `block.parameters == [[:req], [:rest]]`, was not consistently reliable for these cases. This change introduces `determine_block_arity` which: 1. Attempts to parse the original method name from `Proc#to_s` for blocks likely created via `&:method_name`. 2. If the method name is found and the object responds to it, this logic uses the *actual arity of the original method* to correctly determine how `instance_exec` should be called. 3. If the original method name can't be determined, it falls back to using `block.parameters.size`. This ensures methods exposed via `&:method_name` are called with the correct arguments (i.e., with or without `options`), resolving the `ArgumentError`s and removing the need for the previous `rescue` logic. * Fix rubocop offense * Fix(Entity): Enforce zero-arity for Symbol#to_proc in exec_with_object Ruby 3.0’s stricter arity rules meant `&:method_name` blocks could be called with wrong args, causing errors. The old check (`parameters == [[:req],[:rest]]` + `parameters.size`) was unreliable. This update: - Adds `symbol_to_proc_wrapper?` to detect pure `&:method_name` Procs (checks `lambda?`, `source_location.nil?`, and `parameters == [[:req],[:rest]]`). - Introduces `determine_block_arity`, which parses the method name from `block.to_s`; if the object responds to it, it uses `object.method(name).arity`, otherwise falls back to `block.arity`. - In `exec_with_object`, symbol-to-proc wrappers are required to have zero arity (raising `ArgumentError` otherwise), while regular Procs use `block.arity` to decide between `instance_exec(object)` and `instance_exec(object, options)`. This removes rescue logic and ensures `&:method_name` is only used for zero-argument methods. * Correct error message * Fix(Entity): Validate `&:method_name` arity and method presence Ensure symbol-to-proc exposures only work for zero-argument methods: - Raise if the method is undefined on the object - Raise if the method expects one or more arguments - Fall back to `Proc#arity` for regular blocks --------- Co-authored-by: Chau Hong Linh <[email protected]>
1 parent 489f9b9 commit 4d71e0e

File tree

2 files changed

+84
-27
lines changed

2 files changed

+84
-27
lines changed

lib/grape_entity/entity.rb

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -519,19 +519,47 @@ def serializable_hash(runtime_options = {})
519519
end
520520

521521
def exec_with_object(options, &block)
522-
if block.parameters.count == 1
522+
arity = if symbol_to_proc_wrapper?(block)
523+
ensure_block_arity!(block)
524+
else
525+
block.arity
526+
end
527+
528+
if arity.zero?
523529
instance_exec(object, &block)
524530
else
525531
instance_exec(object, options, &block)
526532
end
527-
rescue StandardError => e
528-
# it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc
529-
# accounting for expose :foo, &:bar
530-
if e.is_a?(ArgumentError) && block.parameters == [[:req], [:rest]]
531-
raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0'
533+
end
534+
535+
def ensure_block_arity!(block)
536+
# MRI currently always includes "( &:foo )" for symbol-to-proc wrappers.
537+
# If this format changes in a new Ruby version, this logic must be updated.
538+
origin_method_name = block.to_s.scan(/(?<=\(&:)[^)]+(?=\))/).first&.to_sym
539+
return 0 unless origin_method_name
540+
541+
unless object.respond_to?(origin_method_name, true)
542+
raise ArgumentError, <<~MSG
543+
Cannot use `&:#{origin_method_name}` because that method is not defined in the object.
544+
MSG
532545
end
533546

534-
raise e
547+
arity = object.method(origin_method_name).arity
548+
return 0 if arity.zero?
549+
550+
raise ArgumentError, <<~MSG
551+
Cannot use `&:#{origin_method_name}` because that method expects #{arity} argument#{'s' if arity != 1}.
552+
Symbol‐to‐proc shorthand only works for zero‐argument methods.
553+
MSG
554+
end
555+
556+
def symbol_to_proc_wrapper?(block)
557+
params = block.parameters
558+
559+
return false unless block.lambda? && block.source_location.nil?
560+
return false unless params.size >= 2
561+
562+
params[0].first == :req && params[1].first == :rest
535563
end
536564

537565
def exec_with_attribute(attribute, &block)

spec/grape_entity/entity_spec.rb

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,14 @@ def method_without_args
393393
'result'
394394
end
395395

396+
def method_with_one_arg(_object)
397+
'result'
398+
end
399+
400+
def method_with_multiple_args(_object, _options)
401+
'result'
402+
end
403+
396404
def raises_argument_error
397405
raise ArgumentError, 'something different'
398406
end
@@ -423,28 +431,49 @@ def raises_argument_error
423431
end
424432

425433
context 'with block passed in via &' do
426-
if RUBY_VERSION.start_with?('3')
427-
specify do
428-
subject.expose :that_method_without_args, &:method_without_args
429-
subject.expose :method_without_args, as: :that_method_without_args_again
430-
431-
object = SomeObject.new
432-
expect do
433-
subject.represent(object).value_for(:that_method_without_args)
434-
end.to raise_error Grape::Entity::Deprecated
435-
436-
value2 = subject.represent(object).value_for(:that_method_without_args_again)
437-
expect(value2).to eq('result')
438-
end
439-
else
440-
specify do
441-
subject.expose :that_method_without_args_again, &:method_without_args
434+
specify do
435+
subject.expose :that_method_without_args, &:method_without_args
436+
subject.expose :method_without_args, as: :that_method_without_args_again
442437

443-
object = SomeObject.new
438+
object = SomeObject.new
444439

445-
value2 = subject.represent(object).value_for(:that_method_without_args_again)
446-
expect(value2).to eq('result')
447-
end
440+
value = subject.represent(object).value_for(:method_without_args)
441+
expect(value).to be_nil
442+
443+
value = subject.represent(object).value_for(:that_method_without_args)
444+
expect(value).to eq('result')
445+
446+
value = subject.represent(object).value_for(:that_method_without_args_again)
447+
expect(value).to eq('result')
448+
end
449+
end
450+
451+
context 'with block passed in via &' do
452+
specify do
453+
subject.expose :that_method_with_one_arg, &:method_with_one_arg
454+
subject.expose :that_method_with_multple_args, &:method_with_multiple_args
455+
456+
object = SomeObject.new
457+
458+
expect do
459+
subject.represent(object).value_for(:that_method_with_one_arg)
460+
end.to raise_error ArgumentError, match(/method expects 1 argument/)
461+
462+
expect do
463+
subject.represent(object).value_for(:that_method_with_multple_args)
464+
end.to raise_error ArgumentError, match(/method expects 2 arguments/)
465+
end
466+
end
467+
468+
context 'with symbol-to-proc passed in via &' do
469+
specify do
470+
subject.expose :that_undefined_method, &:unknown_method
471+
472+
object = SomeObject.new
473+
474+
expect do
475+
subject.represent(object).value_for(:that_undefined_method)
476+
end.to raise_error ArgumentError, match(/method is not defined in the object/)
448477
end
449478
end
450479
end

0 commit comments

Comments
 (0)