From cc14f92994151132826648ef7521e4c7a95863e0 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 15 Apr 2026 14:23:39 +0200 Subject: [PATCH 1/4] Fix single-argument lambda regression in exec_with_object PR #389 changed arity detection to use block.arity.zero? which broke single-argument lambdas in expose blocks and if: conditions, causing ArgumentError (given 2, expected 1). Restore parameter count check for regular blocks while keeping symbol-to-proc detection intact. Fixes #398 --- lib/grape_entity/entity.rb | 11 +++---- spec/grape_entity/entity_spec.rb | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index b80bc55..6e4713b 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -525,13 +525,10 @@ def serializable_hash(runtime_options = {}) end def exec_with_object(options, &block) - arity = if symbol_to_proc_wrapper?(block) - ensure_block_arity!(block) - else - block.arity - end - - if arity.zero? + if symbol_to_proc_wrapper?(block) + ensure_block_arity!(block) + instance_exec(object, &block) + elsif block.parameters.one? instance_exec(object, &block) else instance_exec(object, options, &block) diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 0ee975b..8b43066 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -476,6 +476,30 @@ def raises_argument_error end.to raise_error ArgumentError, match(/method is not defined in the object/) end end + + context 'with single-argument lambda' do + it 'passes only the object without raising ArgumentError' do + subject.expose :that_method_without_args do |object| + object.method_without_args + end + + object = SomeObject.new + value = subject.represent(object).value_for(:that_method_without_args) + expect(value).to eq('result') + end + end + + context 'with two-argument lambda' do + it 'passes the object and options without raising ArgumentError' do + subject.expose :that_method_without_args do |object, _options| + object.method_without_args + end + + object = SomeObject.new + value = subject.represent(object).value_for(:that_method_without_args) + expect(value).to eq('result') + end + end end context 'with no parameters passed to the block' do @@ -521,6 +545,32 @@ def raises_argument_error expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value') end + it 'works with single-argument if condition lambdas' do + subject.expose :awesome do + subject.expose(:condition_met, if: ->(_) { true }) { |_| 'value' } + subject.expose(:condition_not_met, if: ->(_) { false }) { |_| 'value' } + end + + expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value') + end + + it 'works with two-argument if condition lambdas' do + subject.expose :awesome do + subject.expose(:condition_met, if: ->(_, _) { true }) { |_| 'value' } + subject.expose(:condition_not_met, if: ->(_, _) { false }) { |_| 'value' } + end + + expect(subject.represent({}).value_for(:awesome)).to eq(condition_met: 'value') + end + + it 'works with single-argument block exposures' do + subject.expose :awesome do + subject.expose(:nested) { |obj| obj.class.name } + end + + expect(subject.represent({}).value_for(:awesome)).to eq(nested: 'Hash') + end + it 'does not represent attributes, declared inside nested exposure, outside of it' do subject.expose :awesome do subject.expose(:nested) { |_| 'value' } From 80cb2f9b4ce1392261b716b6b40e88b93a8da348 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 15 Apr 2026 14:34:13 +0200 Subject: [PATCH 2/4] Clean up ensure_block_arity! dead returns and fix splat-arg edge case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use block.arity == 1 instead of block.parameters.one? so that splat-arg lambdas (->(*args) { }) receive both object and options instead of being misrouted to the single-arg path. Remove dead return values from ensure_block_arity! since the caller no longer uses its return value — it is now purely a validation method. --- lib/grape_entity/entity.rb | 6 +++--- spec/grape_entity/entity_spec.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 6e4713b..2951b0f 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -528,7 +528,7 @@ def exec_with_object(options, &block) if symbol_to_proc_wrapper?(block) ensure_block_arity!(block) instance_exec(object, &block) - elsif block.parameters.one? + elsif block.arity == 1 instance_exec(object, &block) else instance_exec(object, options, &block) @@ -539,7 +539,7 @@ 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 + return unless origin_method_name unless object.respond_to?(origin_method_name, true) raise ArgumentError, <<~MSG @@ -548,7 +548,7 @@ def ensure_block_arity!(block) end arity = object.method(origin_method_name).arity - return 0 if arity.zero? + return if arity.zero? raise ArgumentError, <<~MSG Cannot use `&:#{origin_method_name}` because that method expects #{arity} argument#{'s' if arity != 1}. diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 8b43066..d23b67e 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -500,6 +500,18 @@ def raises_argument_error expect(value).to eq('result') end end + + context 'with splat-argument lambda' do + it 'passes the object and options' do + subject.expose :args_count do |*args| + args.size + end + + object = SomeObject.new + value = subject.represent(object).value_for(:args_count) + expect(value).to eq(2) + end + end end context 'with no parameters passed to the block' do From 01673f0b1f0c8c2d20d413fedfb53044dc0a3422 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 15 Apr 2026 14:43:20 +0200 Subject: [PATCH 3/4] Add tests for remaining exec_with_object call paths Cover single-argument blocks in expose_nil condition and callable :as option. Fix misleading test context names that said "lambda" for do...end blocks which are procs. --- spec/grape_entity/entity_spec.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index d23b67e..f024217 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -27,6 +27,11 @@ expect { subject.expose :name, as: :foo }.not_to raise_error end + it 'supports callable :as with single-argument lambda' do + subject.expose :name, as: ->(obj) { obj[:name].upcase } + expect(subject.represent({ name: 'test' }).serializable_hash).to eq('TEST' => 'test') + end + it 'makes sure that :format_with as a proc cannot be used with a block' do # rubocop:disable Style/BlockDelimiters expect { @@ -148,6 +153,12 @@ def initialize(a, b, c) subject.expose(:c) expect(subject.represent(model, option_a: 100).serializable_hash).to eq(a: 100, b: nil, c: 'value') end + + it 'works with single-argument block' do + subject.expose(:a, expose_nil: false) { |obj| obj.c } + subject.expose(:b) + expect(subject.represent(model).serializable_hash).to eq(a: 'value', b: nil) + end end end @@ -477,7 +488,7 @@ def raises_argument_error end end - context 'with single-argument lambda' do + context 'with single-argument block' do it 'passes only the object without raising ArgumentError' do subject.expose :that_method_without_args do |object| object.method_without_args @@ -489,7 +500,7 @@ def raises_argument_error end end - context 'with two-argument lambda' do + context 'with two-argument block' do it 'passes the object and options without raising ArgumentError' do subject.expose :that_method_without_args do |object, _options| object.method_without_args @@ -501,7 +512,7 @@ def raises_argument_error end end - context 'with splat-argument lambda' do + context 'with splat-argument block' do it 'passes the object and options' do subject.expose :args_count do |*args| args.size From 1304ba76b45140bcec1cefaac31b8ec3e70644c8 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Wed, 15 Apr 2026 14:48:37 +0200 Subject: [PATCH 4/4] Add CHANGELOG entry for #399 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb92e86..cce65c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ #### Fixes +* [#399](https://github.com/ruby-grape/grape-entity/pull/399): Fix `ArgumentError` for single-argument lambdas in `expose` blocks and `if:` conditions - [@numbata](https://github.com/numbata). * Your contribution here. ### 1.0.2 (2026-04-13)