ruby-grape / ruby-grape/grape-entity

Ruby 3: Entities exposing fields via Symbol#to_proc are broken

Open
#354 3 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
729
Forks
154
PR merge metrics
No merged PRs in 30d

Description

With grape-entity, I can write code like the following:

    class Blob < Grape::Entity
      expose :path
      expose :filename, &:path
    end

This utilizes Ruby's somewhat obscure & operator to call Symbol#to_proc and forward filename to path. The proc returned by this function takes the receiver of the method described by the symbol as its first argument, and the actual method parameters as the remaining arguments. Obviously the first argument is required (since it is equivalent to self), but Ruby 2 had a design issue where to_proc for symbols would report signature metadata that is not really correct. See https://rubyreferences.github.io/rubychanges/3.0.html#symbolto_proc-reported-as-lambda for a good summary.

The problem in grape-entity is this:

In Entity::exec_with_object, there is a test where grape reflects on the signature of such a block:

    def exec_with_object(options, &block)
      if block.parameters.count == 1
        instance_exec(object, &block)
      else
        instance_exec(object, options, &block)
      end
    rescue StandardError => e
      ...
    end

Here, block.parameters.count (or, block.arity) will return an incorrect value in Ruby 2:

[4] pry(#<API::Entities::Blob>)> block.parameters
=> [[:rest]]

[6] pry(#<API::Entities::Blob>)> block.arity
=> -1

This is Ruby lying about proc arity, since it tosses all possible arguments into a single optional argument (-1 means all arguments are optional, which is not true; you always need a receiver here.)

Ruby 3 fixes this:

[1] pry(#<API::Entities::Blob>)> block.parameters
=> [[:req], [:rest]]
[4] pry(#<API::Entities::Blob>)> block.arity
=> -2

This is correct: the first argument is now required, since it is the receiver of path. The second argument is the optional parameters (perhaps none.) The arity is also correct now: -2 means 1 required parameter, the rest optional.

What this means is that now grape-entity goes down the wrong code path in exec_with_object, since parameter count is now 2, not 1.

I'm not actually all that familiar with grape, I am just working on the Ruby 3 migration at GitLab so I wanted to raise awareness about this somewhat subtle issue.

A simple workaround is to rewrite the entity like so:

    class Blob < Grape::Entity
      expose :path
      expose :filename do |instance|
        instance.path
      end
    end

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at Entity::exec_with_object and reproduce the expose :filename, &:path example under Ruby 3, comparing block.parameters and block.arity with the dispatch shown in the issue. Trace the surrounding entity execution flow; done means Symbol#to_proc entities follow the correct execution path without breaking ordinary block-based exposes.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.