Normalizing ActiveRecord attributes with a custom DSL
Tags: rails, tutorial, beginners, ruby
Published: 2021-08-26 09:48:53

Lately, I've been reading through some Rails code that I've written for my side-projects. After reading through 4-5 projects, I noticed a common pattern that could be extracted and reused throughout the application. The pattern goes something likes this,

```ruby
class User < ApplicationRecord
  before_validation :normalize_fields

  private

  def normalize_fields
    self.email = email.to_s.downcase.strip
    self.name = name.to_s.strip
    # more normalization
  end
end
```

I'm sure you've seen something like this or most probably you've written some field normalizers yourself.

In my case, I wasn't just normalizing the field in the `User` model, but through `Account`, `Membership`, `Contact` models, and so on. I decided to do myself a favor and took out some time off of my schedule and made up my mind to solve this issue once and for all.

My first instinct was to use a custom Ruby DSL. 
> DSLs are small languages, focused on a particular aspect of a software system - [Martin Fowler](https://martinfowler.com/books/dsl.html).

I tried to look for some inspiration and stumbled across the `has_secure_token` [method](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token). It taps into the `before_create` callback and performs some action, which is analogous to what I was doing. The only difference was to use the `before_validation` callback in our case and perform some normalizations.

## The `normalize` method

The next thing to figure out was to design the API. This was pretty simple as I knew exactly what I wanted to achieve. I also looked at the `normalize` [gem](https://rubygems.org/gems/normalize) and I came up with the following syntax,

```ruby
class User < ApplicationRecord
  normalize :email, with: %i[strip downcase]
  normalize :name, with: :strip
end
```
Now, the only thing left is to write some code.

```ruby
# app/models/concerns/normalize.rb

module Normalize
  extend ActiveSupport::Concern

  class_methods do
    def normalize(*args)
      options = args.extract_options!

      before_validation do
        args.each { |field| send("#{field}=", normalized_value(field, options[:with])) }
      end
    end
  end

  private

  def normalized_value(field, normalizers)
    if normalizers.is_a?(Array)
      normalizers.inject(send(field).to_s, :try)
    else
      send(field).to_s.send(normalizers)
    end
  end
end
```

Firstly, we need to define a `class` method called `normalize`. We extract the `options` from the `normalize` method and then we're left only with the fields that we want to normalize on. For example,

```ruby
normalize :email, :name, with: %i[strip downcase]
```

If we extract the options, we're left with an array of `[:email, :name]`, on which we can loop through and then apply the normalizers.

Secondly, the `with` option can be an `array` of `symbol`s or just a `symbol`. For example,

```ruby
normalize :email, :name, with: %i[strip downcase]
normalize :first_name, with: :strip
```

On the `normalized_value` method, we check for this case and apply the normalizers conditionally. 

I then included this concern in the `ApplicationRecord` and started changing all of the previous occurrences with the newer syntax. I was pretty happy with what I achieved until I came across a code where I was doing something like,

```ruby
class Account < ApplicationRecord
  before_validation :normalize_cname

  private

  def normalize_cname
    self.cname = cname.to_s.downcase.gsub(/\Ahttps?:\/\//, "")
  end
end
```

Now, this isn't possible with the `normalize` method with what we have right now.

## Designing for resilience

I thought to myself that it would be perfect if we could pass a block to the `normalize` method and `call` the block in the `normalized_value` method. Something like this would work perfectly.

```ruby
class Account < ApplicationRecord
  normalize :cname, with: %i[downcase] do |cname|
    cname.gsub(/\Ahttps?:\/\//, "")
  end
end
```

In my opinion, passing in a `block` would be useful in many cases. Not only can we use methods like `gsub`, we can also use our methods for more power.

```ruby
class Account < ApplicationRecord
  normalize :cname do |cname|
    some_method(cname)
  end

  def self.some_method(cname)
    # some logic
  end
end
```

Let's make some changes to the concern.

```ruby
module Normalize
  extend ActiveSupport::Concern

  class_methods do
    def normalize(*args, &block)
      options = args.extract_options!
      normalizers = [block, options[:with]].flatten.compact

      before_validation do
        args.each { |field| send("#{field}=", normalized_value(field, normalizers)) }
      end
    end
  end

  private

  def normalized_value(field, normalizers)
    value = send(field).to_s

    normalizers.each do |normalizer|
      value = if normalizer.respond_to?(:call)
                normalizer.call(value)
              elsif value.respond_to?(normalizer)
                value.send(normalizer)
              end
    end

    value
  end
end
```

This is the final implementation of the `normalize` method. Instead of checking if the `with` option is an array, we now check if it's a `block`.

I'm pretty satisfied with the implementation and the only way to find if it fits all use cases is to use it on different code bases. I'm still on the lookout for more extractions from my existing projects and I'll try to share with you all if I find them useful.

## References

- [has_secure_token](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token)
- [normalize](https://rubygems.org/gems/normalize) gem
- [Cover photo](https://unsplash.com/photos/LKsHwgzyk7c)