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) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index b80bc55..2951b0f 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.arity == 1 instance_exec(object, &block) else instance_exec(object, options, &block) @@ -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 @@ -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}. diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 0ee975b..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 @@ -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 @@ -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' }