Dorian
Dorian

Reputation: 9085

normalize email when doing find_by(email: ...)

I normalize the email addresses in a before_validation, e.g.:

class User
  before_validation do
    self.email = normalized_email
  end

  def normalized_email
    User.normalize_email(email)
  end
end

But then I have to do User.find_by(email: User.normalize_email(params[:email])) everywhere for instance, and it bit me today when trying to find an user I forgot to normalize the email address, so I would like to have it done automatically.

Ideally I would not overwrite find_by and it would work for all the methods, like where for instance.

How can I do it?

Upvotes: 0

Views: 258

Answers (3)

Dorian
Dorian

Reputation: 9085

Not perfect, but here is what I did:

scope :by_email, lambda { |email| where(email: Normalizer.normalize_email(email)) }

User.kept.by_email(email).first
User.by_email(email).first!

Edit: Removed my monkey patch, it was not working anyway

Upvotes: 1

Schwern
Schwern

Reputation: 164829

You can override find_by in User to ensure this happens consistently.

  class << self
    def find_by(*args)
      # find_by can be called without a Hash, make sure.
      attrs = args.first
      super unless attrs.is_a?(Hash)

      # Cover both calling styles, find_by(email: ...) and find_by("email": ...)
      [:email, "email"].each do |key|
        # Be careful to not add email: nil to the query.
        attrs[key] = normalized_email(attrs[key]) if attrs.key?(key)
      end

      super
    end
  end

This also covers find_by!, find_or_*, and create_or_find_by* methods. It doesn't cover where nor raw SQL.

You could override where, but there's some odd caching happening which makes that troublesome.

Upvotes: 1

spickermann
spickermann

Reputation: 106882

I would just define a special method for this use-case:

class User
  def self.by_email(email)
    find_by(email: User.normalize_email(email))
  end

  before_validation do
    self.email = normalized_email
  end

  def normalized_email
    User.normalize_email(email)
  end
end

Which can then be used like this:

User.by_email(params[:email])

Please note that I explicitly didn't suggest using a scope for this, because by convention scopes should be chainable and therefore should return ActiveRecord::Relations what doesn't work well when you only want to return a single record.

Upvotes: 6

Related Questions