Skip to content

Fix the method Grape::Entity#exec_with_object to work with Ruby 3 #376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

linhchauatx
Copy link

Fix the method Grape::Entity#exec_with_object to work with Ruby 3.

@dblock
Copy link
Member

dblock commented Jul 27, 2023

We already have Ruby 3.x in the CI. Are we missing tests? (Add part of this PR please)?

Don't worry about versions, revert that.

@@ -523,6 +523,7 @@ def exec_with_object(options, &block)
# 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]]
Rails.logger.error("***** ERROR in exec_with_object: #{e.message}\n#{e.backtrace.join("\n")}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not everyone uses Rails.

Copy link
Member

@dblock dblock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a fix but also still has a Rails mention, needs work.

@numbata numbata mentioned this pull request Jun 1, 2025
@numbata
Copy link
Collaborator

numbata commented Jun 2, 2025

After working through the #389 and seeing how our &:method shorthand behaves, I've come to believe that maybe it's better to deprecate this feature entirely. Here's why:

Right now:

class FooObject
  def method_without_args
    'result'
  end
end

class FooObjectEntity < Grape::Entity
  # These two lines do exactly the same thing:
  expose :boo,                &:method_without_args
  expose :method_without_args, as: :boo_again
end

foo = FooObject.new
FooObjectEntity.represent(foo).serializable_hash
# => { boo: "result", boo_again: "result" }

In other words:

expose :alias, &:method_name
# is identical to:
expose :method_name, as: :alias

Because there’s no extra behavior or transformation happening, the shorthand doesn't buy us anything over the already-concise as: form. If our goal is in "expose a zero-argument method under a different key," we can do that directly.

The usage of &:method causes confusion and extra complexity in context of grape-entity:

  1. Misleading “format” vs. “attribute” semantics
    Some users could expect &:method to behave like a formatter (e.g. “call my own entity helper named method”), but in fact it always calls the method on the model. That confusion led to a lot of special casing in exec_with_object)
  2. Always requires zero arguments
    We now have to detect “symbol-to-proc” blocks, lookup the original method’s arity, and raise if it isn’t zero. That logic lives in exec_with_object and complicates our code path.
  3. No real shorthand advantage
    Compared to expose :foo, as: :bar, the only thing &:method_name does is skip the as: keyword. But you still end up writing two tokens(:foo, &:bar) instead of just more transparent :bar, as: :foo.

Because of these points, the "nice little shorthand" is actually increasing our maintenance burden and confusing people more than helping them (in my subjective opinion).

Another idea is to make &:method behave more like a mini-formatter—something akin to format_with:. For instance:

class FooObject
  def name    = "name"
  def surname = "surname"
end

class FooObjectEntity < Grape::Entity
  # Treat `&:capitalize` as “call my own `capitalize` helper”
  expose :name, &:capitalize
  expose :surname, &:capitalize

  # If `value` is the raw attribute value (e.g. "name"),
  # `object` and `options` can be used, too.
  def capitalize(value, object = nil, options = nil)
    value.upcase
  end
end

foo = FooObject.new
FooObjectEntity.represent(foo).serializable_hash
# => { name: "NAME", surname: "SURNAME" }

In this proposal, &:capitalize doesn’t mean "call object.capitalize" (which might not even exist), but instead delegates to a method defined inside the entity class. That would give us a lightweight way to define per-entity formatting logic without needing a full format_with block.

wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants