Don't mix Forwardable and ActiveSupport::Delegate
Nick Sutterer
Posted on December 2, 2021
A few days ago I released the disposable
gem minor line 0.6.0. The goal was to remove unnecessary coupling to the old uber
gem that was once planned to hold a set of abstractions we need in all Trailblazer gems.
What I mainly worked on was removing Uber::Delegate
and supersede it with Ruby's built-in Forwardable
. We simply need to delegate a bunch of methods from one object to another, and Forwardable
does just that.
So what the new code basically did was to extend the Disposable::Twin
class and use def_delegators
to create automatic delegations.
class Disposable::Twin
extend Forwardable # this used to be Uber::Delegate
def_delegators :model, :title, :title=
def_delegators :model, :email, :email=
Obviously, removing gem dependencies, even if they're your own, feels good, so I pushed this new gem after running tests in two projects and leaned back in my pricey office desk.
Here comes another delegate!
A day later we received bug reports from several users. All of them were using Reform form objects, the problem was thrown from Reform::Form
classes they had in their app.
ArgumentError:
wrong number of arguments (given 2, expected 1)
# ruby/3.0.0/forwardable.rb:133:in `instance_delegate'
The developers reported that before the upgrade to Disposable 0.6.0
, code as the following worked fine.
class CreateForm < Reform::Form
delegate :assignment, to: :model
end
After the upgrade, this would break with the ArgumentError
illustrated above. My first suspicion was that this is some code not ready for Ruby 3, as this kind of exception is often observed in older code that runs with Ruby 3.
Delegate or delegate?
An hours going through the Forwardable
code and playing with the broken example app provided by some nice user didn't bring any progress.
At some point I realized that what the users were actually using to create delegations was the delegate
method not from Forwardable
but from ActiveSupport::Delegate
. The docs enlightened me that this module mixes a method #delegate
into the class - the method that was used by all users reporting the error.
Order matters!
It feels really stupid in hindsight, and it took at least two hours to understand the problem.
Here's the chain of sparks that lead to fixing the problem.
- A
Reform::Form
class inherits fromTwin::Disposable
. This means that eachForm
class getsForwardable
methods mixed in. Of course, only after upgradingdisposable
to 0.6.0 where we automatically includedForwardable
intoTwin
. - Before 0.6.0, the former delegation from
Uber::Delegate
only mixed in one method called#delegates
(mind thes
). - The
Forwardable
module includes a method called#delegate
. - Our nasty
ActiveSupport::Delegate
module also has a method called#delegate
- just likeForwardable
! - In some Rails apps, delegation from ActiveSupport is automatically included into all classes.
The problem we were facing was that ActiveSupport and its delegation code was included automatically into the Reform::Form
class before the disposable
gem included Forwardable
and hence the latter overriding the #delegate
method you are expecting.
In other words, Forwardable
killed ActiveSupport's #delegate
method as it was loaded and included later.
Problem solved
We decided to removed Forwardable
from disposable
for now and fixed the obscure issue. When using delegate
in a form it now refers to ActiveSupport's implementation.
Posted on December 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024