Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 6 additions & 9 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.arity == 1
instance_exec(object, &block)
else
instance_exec(object, options, &block)
Expand All @@ -542,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
Expand All @@ -551,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}.
Expand Down
73 changes: 73 additions & 0 deletions spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -476,6 +487,42 @@ def raises_argument_error
end.to raise_error ArgumentError, match(/method is not defined in the object/)
end
end

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
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 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
end

object = SomeObject.new
value = subject.represent(object).value_for(:that_method_without_args)
expect(value).to eq('result')
end
end

context 'with splat-argument block' 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
Expand Down Expand Up @@ -521,6 +568,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' }
Expand Down