Ahmed Nadar

May 21, 2024

Normalization in Rails 7.1 era


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 all Rails developers live happily ever after Rails 7.1 period.
The end.

~~~~~
Coming to RailsWorld?
Check out my series, Ahmed’s Unofficial RailsWorld Guide to Toronto! This first-of-its-kind guide is perfect for RailsWorld attendees. I write about the history of the amazing venue, Evergreen Brickworks, take you on a ride from the airport to the city, hotels and around, and explore Toronto's best spots to eat, visit, and have fun. Whether you're attending the conference or just visiting the city, you'll find something valuable.


Yours,
Ahmed Nadar

About Ahmed Nadar

Ruby on Rails enthusiast at heart. I run RapidRails agency focus on Rails development & UI stack. Maker of RapidRails UI component for Ruby on Rails.
Find me on Twitter – @ahmednadar