Prepending Modules to the Rescue
Tags: ruby, rails, productivity
Published: 2020-11-26 14:26:25

Ruby's ability to _"overlay"_ default implementations of constants, methods and variables via the [`prepend`](https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend) or [`prepend_features`](https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend_features) method on `Module` can be a helpful tool when dealing with gems overriding setter methods for attributes.

## Scenario 🧫

On a [Ruby on Rails](https://rubyonrails.org/) project, we were recently migrating from the [`attr_encrypted`](https://github.com/attr-encrypted/attr_encrypted) to the [`lockbox`](https://github.com/ankane/lockbox) gem for encrypting database fields. For live applications which cannot accept hours of downtime, the migration path is a multi-step process which includes a period where both gems are used side by side until all data has been migrated from the old columns to the new columns.

Both gems integrate into Rails models via their own macro-style methods one is supposed to add to the models' class definitions:

```ruby
class User < ApplicationRecord
  attr_encrypted :email, key: key
  attr_encrypted :phone, key: key

  encrypts :email, :phone, migrating: true
end
```

Under the hood, both gems then dynamically generate the respective getter and setter methods for the attributes (`email`, `email=`, `phone` and `phone=` in this example).

In addition to that, we were also overriding the setter methods `email=` and `phone=` ourselves to do some normalization on the provided values before assigning them. Combining this with the generated setters from `attr_encrypted` introduces a lot of fuzz: in what order are the implementations called - if at all - and what does `super` mean in which context? In order to eliminate all this confusion from the start, we previously decided to make our own implementation the _"source of truth"_ and instead of relying on `super` calls, just integrate the respective parts of `attr_encrypted`'s implementation into our own:

```ruby
def phone=(value)
  normalized_number = Normalizers::PhoneNumber.normalize(value)
  self.encrypted_phone = encrypt(:phone, normalized_number)
  instance_variable_set(:@phone, normalized_number)
end
```

## Problem 💥

Now, during the migration phase were we utilize both gems in parallel, our own implementation would of course also need to integrate the internals of both gems in our own overriden implementation. Plus we would need to ensure the `attr_encrypted` related code is removed again once the migration phase is over 🤯.

This seemed overwhelming and just way too many things to take care of for our own tiny model implementation. Integrating gems should ideally not interfere too much with our own plans to normalize attributes before assigning them. In addition to that, integrating so deeply with a gem, that understanding the code required reading the gems internals beyond the _"normal"_ instructions in the README also comes with a high price on maintainability.

So we needed to find another way to use both gems in parallel while also guaranteeing our values are normalized **before** assigning and **before** encrypting them.

## Rescue 🚑

A [post on the arkency blog](https://blog.arkency.com/2016/02/using-anonymous-modules-and-prepend-to-work-with-generated-code/) describes how Ruby's [`prepend`](https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend) method on `Module` can be utilized to override or better _"overlay"_ methods added directly onto the class by a gem. One can prepend an anonymous module inline with the own implementation to either fully override the gem's implementation or just _"prepend"_ one's own implementation and then call `super()` to still invoke the code generated by the gem.

We simply want to _"prepend"_ our own normalization step before the gems start to do their magic and ideally don't want to get into the details of what they are actually doing. So utilizing `super()` after the normalization step fits our use case perfectly. In the model class definition, this could look like this:

```ruby
class User < ApplicationRecord
  attr_encrypted :email, key: key
  attr_encrypted :phone, key: key

  encrypts :email, :phone, migrating: true

  prepend(Module.new do
    def phone=(value)
      normalized_number = Normalizers::PhoneNumber.normalize(value)
      super(normalized_number)
    end
  end)
end
```

This _"overlays"_ or _"prepends"_ our normalization step before the class' implementation of the setter method, even when it's changed by any of the included gems. So we make sure the value is normalized before it is assigned and the gems' implementation of the setter invoked afterwards without any need for us to fiddle with internal details.

⚠️ **Note** however, that for the sake of readability and consistency, our model class definition follows Rubocop's [Rails Style Guide](https://github.com/rubocop-hq/rails-style-guide#macro-style-methods):

> Group macro-style methods (`has_many`, `validates`, etc) in the beginning of the class definition.

Following this class layout, our own implementation is sure to be processed after the gems' methods were defined and we are prepending our normalization step before the final definition of the setter method. Ordering the definitions differently in our class would impact the value of `super()` here.

## Cleanup 🧽

While prepending an anonymous module directly inside the class definitions works perfectly fine for our use case here, it still looks very verbose and suspiciously distracting for anyone reading over the model definition. And as we made use of this technique in multiple models within the project, we extracted the boilerplate into a [model concern](https://api.rubyonrails.org/classes/ActiveSupport/Concern.html). The goal was to hide the details of the prepending trick while at the same time making the normalization step more visible and explicit to the reader.

We extracted a more general `Normalizable` concern. We already had multiple normalizer classes in the project. They all follow the same pattern and expose a single `normalize` class method as their public API. So it just made sense tie the implementation of the `Normalizable` model concern close to those normalizers:

```ruby
module Normalizable
  extend ActiveSupport::Concern

  class_methods do
    def normalize(attr, with:)
      prepend(Module.new do
        define_method("#{attr}=") do |value|
          normalized_value = with.public_send(:normalize, value)
          super(normalized_value)
        end
      end)
    end
  end
end
```

Utilizing this, we can change the previous example to:

```ruby
class User < ApplicationRecord
  include Normalizable

  attr_encrypted :email, key: key
  attr_encrypted :phone, key: key

  encrypts :email, :phone, migrating: true

  normalize :phone, with: Normalizers::PhoneNumber
end
```

This hides the internal details of our own implementation of the setter while still making it explicit that we are doing a normalization on the attribute. Similar to the original implementation inlining `prepend` with an anonymous module, this approach of course still only works as intended if the `normalize` macro in the class definition is defined **after** any other setter methods generated by gems are defined. However, in our case it seemed most fitting to place the `normalize calls at the end of the macros section anyways.