Normalization in Rails 7.1 era

Ahmed NadarAhmed Nadar
2 min read

Before Rails 7.1

Once upon a time, way long before the Rails 7.1 era, a smart Rails developer (like yourself) needed to ensure user email addresses were properly normalized (sanitized and formatted correctly). Back then, they used clever techniques such as callbacks like before_save and before_validation, attribute setters, or even the normalize gem to get the job done. Here are some of those old tricks and tips on how they used to do it.

# normalize with before_save callback
class User < ApplicationRecord
  before_save :sanitize_email

  private

  def sanitize_email
    self.email = email.strip.downcase
  end
end

# normalize with before_validation callback
class User < ApplicationRecord
  before_validation :sanitize_email

  private

  def sanitize_email
    self.email = email.strip.downcase
  end
end

# override the setter from ActiveRecord
class User < ApplicationRecord
  def email=(value)
    super(value.strip.downcase)
  end
end

# Don't like callbacks? Use the normalize gem in `app/normalizers`
class EmailNormalizer
  def self.call(email)
    email.strip.downcase
  end
end

class User < ApplicationRecord
  normalize :email, with: EmailNormalizer
end

After Rails 7.1

As time passed, the Rails community reached the Rails 7.1 era. A group of those smart Rails developers (maybe it's you) gathered around the Rails core team and agreed on a better way to normalize attributes. They came up with a nifty idea.

Imagine having a ClassMethod normalizes that comes with a set of rules, such as converting all email addresses to lowercase, removing leading/trailing whitespace, or enforcing a specific format before they are saved to the database. This "Normalization" class reduces data redundancy and minimizes inconsistencies. Also, it organizes data in a structured and consistent way, making it easier to query, update, and maintain.

The Rails core team wanted to make it easy for today's and future Rails developers by providing a simple API for model attributes. All developers need to do is pass the attribute's name. Here is how they demonstrated their solution. Pow 💥

class User < ActiveRecord::Base
  normalizes :email, with: -> email { email.strip.downcase }
  normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
end

user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
user.email                  # => "cruise-control@example.com"

user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
user.email                  # => "cruise-control@example.com"
user.email_before_type_cast # => "cruise-control@example.com"

User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count         # => 1
User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0

User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")         # => true
User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false

User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"

And Rails developers live happily ever after Rails 7.1 period. The end.

0
Subscribe to my newsletter

Read articles from Ahmed Nadar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ahmed Nadar
Ahmed Nadar

Developer and Product Design. RapidRails UI components creator Run RapidRails Agency. https://rapidrails.cc