How to Generate Unique Names With a Plain Old Ruby Object

16 November 2023

Photo by Jon Tyson on Unsplash

I recently came accross something approximating the following in a client’s code base.

# app/model/user.rb
class User < ActiveRecord::Base
  # ...more code
  before_validation :set_username
  # ...more code

  private

  def set_username
    return unless username.blank?

    i = 0
    begin
      if i < 10 && (email.present? || first_name.present?)
        prefix = [first_name, last_name].compact.join.presence || email.split('@')[0]
        self.username = prefix + (i.positive? ? i.to_s : '')
      else
        self.username = "user_#{SecureRandom.hex(3)}"
      end

      i += 1
    end while User.exists?(username: self.username)
  end
end

It’s a fairly standard requirement for an application to have some named record that may need an automatically generated name that must be unique. This is a pretty standard (if not pretty) approach for a Rails application to take. I’ve taken a similar tack in the past myself.

This time1 it occurred to me that this could be a good opportunity to apply a little bit of Object-Oriented Design and make use of a new object. As a starting place I envisioned something like this:

# app/model/user/unique_name.rb
class User::UniqueName
  delgegate :email, :first_name, :last_name, to: :@user

  def initialize(user)
    @user = user
  end

  def to_s
    username = nil

    i = 0
    begin
      if i < 10 && (email.present? || first_name.present?)
        prefix = [first_name, last_name].compact.join.presence || email.split('@')[0]
        username = prefix + (i.positive? ? i.to_s : '')
      else
        username = "user_#{SecureRandom.hex(3)}"
      end

      i += 1
    end while User.exists?(username: self.username)

    username
  end
end

# app/model/user.rb
class User < ActiveRecord::Base
  # ...more code
  before_validation :set_username
  # ...more code

  private

  def set_username
    return unless username.blank?

    self.username = UniqueName.new(self)
  end
end

Although User::UniqueName still isn’t terribly pretty, it already has some useful properties. For one, in any testing I do, whether it’s in the form of unit tests or experimenting in the REPL, it has the very desirable property that it can be tested on basis of simple input and output. Additionally, this can be done in relative isolation from the other properties I’d like to test about User. This wouldn’t stop me from testing the integration with User either, but those tests can be few.

User::UniqueName.new(User.new).to_s # that's it!

All the essential behavior is encapsulated here. As a nice ergonomic bonus ActiveRecord will take care of calling #to_s for us as it coerces our object into a string.

If this is the best we can do this is not a terrible place to be, but as it stands this code can be cleaned up, and it could be made more general. For example, on another project I’m working on there are many models that require unique names to be generated. Some models also need to be unique within a certain scope. In that case the object might take this form instead. Here we’ve also added logic that will return the name of the record if it’s present removing the need for a condition in our callback.

# app/models/unique_name.rb
class UniqueName
  def initialize(record, attribute: :name, scope: nil, root_name: nil)
    @record = record
    @attribute = attribute
    @root_name = root_name || "New #{model.model_name.human}"
    @scope = scope
  end

  def to_s
    name = record_name
    return name if name.present?

    unique_name
  end

  def record_name
    record.public_send(attribute)
  end

  def record_scope_value
    record.public_send(scope)
  end

  def unique_name
    n = auto_named_count
    n.zero? ? root_name : "#{root_name} (#{n})"
  end

  def auto_named_count
    query = model.where(attribute => root_name).or(model.where(attribute => "#{root_name} (%)"))
    return query.count unless scope

    query.or(model.where(scope => record_scope_value)).count
  end

  def model
    record.class
  end

  private

  attr_reader :record, :attribute, :root_name, :scope
end

# app/model/user.rb
class User < ActiveRecord::Base
  # ...more code
  before_validation { self.username = UniqueName.new(self) }
  # ...more code
end

# app/model/survey.rb
class Survey < ActiveRecord::Base
  # ...more code
  validates :name, uniqueness: { scope: :author }
  before_validation { self.name = UniqueName.new(self, scope: :author_id) }
  # ...more code
end


# app/model/saved_report.rb
class SavedReport < ActiveRecord::Base
  # ...more code
  validates :name, uniqueness: { scope: :author }
  before_validation { self.name = UniqueName.new(self, scope: :author_id) }
  # ...more code
end

Now we have an object that is general enough to be used widely throughout a large project, and perhaps is on it’s way to being a useful library.


  1. Having long favored functional programming I’ve been exploring the complementary nature of object-oriented and functional programming (more on that later).