diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 9f01031..38b2701 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -519,19 +519,47 @@ def serializable_hash(runtime_options = {}) end def exec_with_object(options, &block) - if block.parameters.count == 1 + arity = if symbol_to_proc_wrapper?(block) + ensure_block_arity!(block) + else + block.arity + end + + if arity.zero? instance_exec(object, &block) else instance_exec(object, options, &block) end - rescue StandardError => e - # it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc - # accounting for expose :foo, &:bar - if e.is_a?(ArgumentError) && block.parameters == [[:req], [:rest]] - raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' + end + + def ensure_block_arity!(block) + # MRI currently always includes "( &:foo )" for symbol-to-proc wrappers. + # If this format changes in a new Ruby version, this logic must be updated. + origin_method_name = block.to_s.scan(/(?<=\(&:)[^)]+(?=\))/).first&.to_sym + return 0 unless origin_method_name + + unless object.respond_to?(origin_method_name, true) + raise ArgumentError, <<~MSG + Cannot use `&:#{origin_method_name}` because that method is not defined in the object. + MSG end - raise e + arity = object.method(origin_method_name).arity + return 0 if arity.zero? + + raise ArgumentError, <<~MSG + Cannot use `&:#{origin_method_name}` because that method expects #{arity} argument#{'s' if arity != 1}. + Symbol‐to‐proc shorthand only works for zero‐argument methods. + MSG + end + + def symbol_to_proc_wrapper?(block) + params = block.parameters + + return false unless block.lambda? && block.source_location.nil? + return false unless params.size >= 2 + + params[0].first == :req && params[1].first == :rest end def exec_with_attribute(attribute, &block) diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 42fe0f1..7139fec 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -393,6 +393,14 @@ def method_without_args 'result' end + def method_with_one_arg(_object) + 'result' + end + + def method_with_multiple_args(_object, _options) + 'result' + end + def raises_argument_error raise ArgumentError, 'something different' end @@ -423,28 +431,49 @@ def raises_argument_error end context 'with block passed in via &' do - if RUBY_VERSION.start_with?('3') - specify do - subject.expose :that_method_without_args, &:method_without_args - subject.expose :method_without_args, as: :that_method_without_args_again - - object = SomeObject.new - expect do - subject.represent(object).value_for(:that_method_without_args) - end.to raise_error Grape::Entity::Deprecated - - value2 = subject.represent(object).value_for(:that_method_without_args_again) - expect(value2).to eq('result') - end - else - specify do - subject.expose :that_method_without_args_again, &:method_without_args + specify do + subject.expose :that_method_without_args, &:method_without_args + subject.expose :method_without_args, as: :that_method_without_args_again - object = SomeObject.new + object = SomeObject.new - value2 = subject.represent(object).value_for(:that_method_without_args_again) - expect(value2).to eq('result') - end + value = subject.represent(object).value_for(:method_without_args) + expect(value).to be_nil + + value = subject.represent(object).value_for(:that_method_without_args) + expect(value).to eq('result') + + value = subject.represent(object).value_for(:that_method_without_args_again) + expect(value).to eq('result') + end + end + + context 'with block passed in via &' do + specify do + subject.expose :that_method_with_one_arg, &:method_with_one_arg + subject.expose :that_method_with_multple_args, &:method_with_multiple_args + + object = SomeObject.new + + expect do + subject.represent(object).value_for(:that_method_with_one_arg) + end.to raise_error ArgumentError, match(/method expects 1 argument/) + + expect do + subject.represent(object).value_for(:that_method_with_multple_args) + end.to raise_error ArgumentError, match(/method expects 2 arguments/) + end + end + + context 'with symbol-to-proc passed in via &' do + specify do + subject.expose :that_undefined_method, &:unknown_method + + object = SomeObject.new + + expect do + subject.represent(object).value_for(:that_undefined_method) + end.to raise_error ArgumentError, match(/method is not defined in the object/) end end end