Skip to content

Commit ec9c91d

Browse files
committed
Use parameters over arity, add curated error for required kwargs
Switches from Method#arity math to Method#parameters inspection for more precise required-arg detection. Required keyword arguments now produce a curated ArgumentError instead of falling through to Ruby's native error. Adds a spec for the required-kwargs case and sharpens the delegation and method_missing NOTE comments.
1 parent a4d280f commit ec9c91d

3 files changed

Lines changed: 49 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
* Your contribution here.
1010
* [#404](https://github.com/ruby-grape/grape-entity/pull/404): Drop `MultiJson` dependency, use `Hash#to_json` for ActiveSupport-aware serialization - [@numbata](https://github.com/numbata).
11-
* [#406](https://github.com/ruby-grape/grape-entity/pull/406): Handle symbol-to-proc wrappers (`&:method_name`) where the method uses `delegate` or `method_missing` - [@marcrohloff](https://github.com/marcrohloff).
11+
* [#406](https://github.com/ruby-grape/grape-entity/pull/406): Handle symbol-to-proc wrappers (`&:method_name`) where the method uses `delegate` or `method_missing`, and let unknown methods raise a native `NoMethodError` - [@marcrohloff](https://github.com/marcrohloff).
1212

1313
### 1.0.4 (2026-04-17)
1414

lib/grape_entity/entity.rb

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -539,32 +539,48 @@ def ensure_block_arity!(block)
539539
return unless match # Unrecognized format -> bail safe rather than misidentify
540540

541541
origin_method_name = match[:name].to_sym
542-
required_positional_arg_count, variadic = positional_arity_for(origin_method_name)
542+
required_positional_arg_count, required_keyword_arg_count, variadic_positional =
543+
arity_requirement_for(origin_method_name)
543544
return unless required_positional_arg_count
544545

545-
expected_suffix = required_positional_arg_count == 1 ? 'argument' : 'arguments'
546-
expected_suffix += ' or more' if variadic
546+
required_arguments =
547+
required_arguments_summary(required_positional_arg_count, required_keyword_arg_count, variadic_positional)
547548

548549
raise ArgumentError, <<~MSG
549-
Cannot use `&:#{origin_method_name}` because that method expects #{required_positional_arg_count} #{expected_suffix}.
550+
Cannot use `&:#{origin_method_name}` because that method expects #{required_arguments}.
550551
Symbol-to-proc shorthand only works for methods that can be called with no arguments.
551552
MSG
552553
end
553554

554-
def positional_arity_for(method_name)
555+
def arity_requirement_for(method_name)
555556
origin_method = object.method(method_name)
556-
return nil if origin_method.parameters.any? { |type, _| type == :keyreq }
557+
parameters = origin_method.parameters
557558

558-
arity = origin_method.arity
559-
required_positional_arg_count = arity >= 0 ? arity : -arity - 1
560-
return nil if required_positional_arg_count.zero?
559+
required_positional_arg_count = parameters.count { |type, _| type == :req }
560+
required_keyword_arg_count = parameters.count { |type, _| type == :keyreq }
561+
return nil if required_positional_arg_count.zero? && required_keyword_arg_count.zero?
561562

562-
[required_positional_arg_count, arity.negative?]
563+
[required_positional_arg_count, required_keyword_arg_count, parameters.any? { |type, _| type == :rest }]
563564
rescue NameError
564565
# Delegation wrappers and method_missing proxies may not expose a Method; let Ruby raise natively at call time.
565566
nil
566567
end
567568

569+
def required_arguments_summary(required_positional_arg_count, required_keyword_arg_count, variadic_positional)
570+
parts = []
571+
unless required_positional_arg_count.zero?
572+
suffix = required_positional_arg_count == 1 ? 'argument' : 'arguments'
573+
suffix += ' or more' if variadic_positional
574+
parts << "#{required_positional_arg_count} #{suffix}"
575+
end
576+
unless required_keyword_arg_count.zero?
577+
suffix = required_keyword_arg_count == 1 ? 'keyword argument' : 'keyword arguments'
578+
parts << "#{required_keyword_arg_count} #{suffix}"
579+
end
580+
581+
parts.join(' and ')
582+
end
583+
568584
def symbol_to_proc_wrapper?(block)
569585
params = block.parameters
570586

spec/grape_entity/entity_spec.rb

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ def method_with_optional_kwargs_as_a_splat(**_kwargs)
424424
'result'
425425
end
426426

427+
def method_with_required_kwargs(_required:)
428+
'result'
429+
end
430+
427431
def method_with_optional_args_and_kwargs_as_a_splat(*_args, **_kwargs)
428432
'result'
429433
end
@@ -570,7 +574,7 @@ def block.to_s
570574
end
571575

572576
context 'with block passed in via & that references a method with optional args' do
573-
it 'succeeds if there no required arguments' do
577+
it 'succeeds if there are no required arguments' do
574578
subject.expose :that_method_with_only_optional_args, &:method_with_only_optional_args
575579
subject.expose :method_with_only_optional_args, as: :that_method_with_only_optional_args_again
576580

@@ -590,7 +594,7 @@ def block.to_s
590594

591595
expect do
592596
subject.represent(object).value_for(:that_method_with_required_and_optional_args)
593-
end.to raise_error ArgumentError, include('method expects 1 argument or more.')
597+
end.to raise_error ArgumentError, include('method expects 1 argument.')
594598

595599
expect do
596600
subject.represent(object).value_for(:that_method_with_required_and_optional_args_again)
@@ -641,6 +645,18 @@ def block.to_s
641645
end
642646
end
643647

648+
context 'with block passed in via & that references a method with required kwargs' do
649+
it 'raises an `ArgumentError` if there are required keyword arguments' do
650+
subject.expose :that_method_with_required_kwargs, &:method_with_required_kwargs
651+
652+
object = SomeObject.new
653+
654+
expect do
655+
subject.represent(object).value_for(:that_method_with_required_kwargs)
656+
end.to raise_error ArgumentError, include('method expects 1 keyword argument.')
657+
end
658+
end
659+
644660
context 'with block passed in via & that references a method with optional args and kwargs as a splat' do
645661
specify do
646662
subject.expose :that_method_with_optional_args_and_kwargs_as_a_splat, &:method_with_optional_args_and_kwargs_as_a_splat
@@ -674,8 +690,8 @@ def block.to_s
674690

675691
object = SomeObjectWithMethodMissing.new
676692

677-
# NOTE: ensure_block_arity! cannot pierce delegation wrappers (arity -1);
678-
# Ruby raises a native error at call time instead of the curated grape-entity message.
693+
# NOTE: object.method(:method_with_args) is not available for this method_missing proxy,
694+
# so Ruby raises a native error at call time instead of the curated grape-entity message.
679695
expect do
680696
subject.represent(object).value_for(:that_method_with_args)
681697
end.to raise_error ArgumentError, /given 0, expected 1/
@@ -705,8 +721,8 @@ def block.to_s
705721

706722
object = SomeObjectWithDelegation.new
707723

708-
# NOTE: ensure_block_arity! cannot pierce delegation wrappers (arity -1);
709-
# Ruby raises a native error at call time instead of the curated grape-entity message.
724+
# NOTE: delegation wrappers expose variable arity, so Ruby raises a native error
725+
# at call time instead of the curated grape-entity message.
710726
expect do
711727
subject.represent(object).value_for(:that_method_with_args)
712728
end.to raise_error ArgumentError, /given 0, expected 1/

0 commit comments

Comments
 (0)