Just about every time I start a new Rails project, I find myself building out a way to reference records based on a masked identifier. It just feels wrong to me to display the count of the number of records in the database, which is the Rails default.

UPDATE: I created a Rails plugin that makes it easy to add a non-incrementing masked identifier to any ActiveRecord object. Check out the Github page for more info:

http://github.com/matthodan/masked-identifier

To solve this problem, I built a couple libraries that a) generate a guaranteed-to-be-unique code to use for a masked identifier and b) automatically add the masked identifier code to new records when they are saved for the first time.

Here is the code for the code generator:

# CodeGenerator creates unique codes based on a column of an ActiveRecord object
#
# Usage:
# 1) Copy this file into your [rails project]/lib directory
# /lib/code_generator.rb
#
# 2) Add an initializer to your Rails project
# require 'code_generator'
#
# 3) Generate a unique code based on an ActiveRecord model
# CodeGenerator.unique_code(YourModel, 'name_of_unique_column', length_of_code)

class CodeGenerator
  def self.unique_code(klass, field, size)
    code = self.random_code(size)
    until self.code_is_unique?(klass, field, code)
      code = self.random_code(size)
    end
    return code
  end

  private
  def self.code_is_unique?(klass, field, code)
    return true if klass.class.send('find_by_' + field, code).nil?
  end

  def self.random_code(size)
    charset = %w{ 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H J K L M N O P Q R T U V W X Y Z }
    (0...size).map { charset.to_a[rand(charset.size)] }.join
  end
end

Here is the code to automatically add a masked identifier:

# Include this module in an ActiveRecord object with a column named 'masked_identifier' to
# automatically generate a masked identifier when a new record is saved that can then be used
# to lookup records without revealing how many records have been created
#
# Usage:
# 1) Copy this file into your [rails project]/lib directory
# /lib/has_masked_identifier.rb
#
# 2) Add an initializer to your Rails project
# require 'has_masked_identifier'
#
# 3) Add 'has_masked_identifier' to your ActiveRecord model
# class YourModel < ActiveRecord::Base
#   has_masked_identifier
# end
#
# Optionally, you can include a hash as a parameter to has_masked_identifier that includes a
# custom named column and/or length to override the default 'masked_identifier' column name 
# and/or default length of 8 characters:
# class YourModel < ActiveRecord::Base
#   has_masked_identifier column: 'some_other_column', length: 15
# end

module HasMaskedIdentifier
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    attr_accessor :masked_identifier_column, :masked_identifier_length

    def has_masked_identifier(options = {})
      send :include, InstanceMethods
      self.masked_identifier_column = options[:column] || 'masked_identifier'
      self.masked_identifier_length = options[:length] || 8
      before_save :add_masked_identifier, on: :create
    end
  end

  module InstanceMethods
    private

    def add_masked_identifier
      column = self.class.masked_identifier_column
      length = self.class.masked_identifier_length
      raise ArgumentError, 'Column does not exist' unless self.respond_to?(column)
      self[column.to_sym] = CodeGenerator.unique_code(self, column, length)
    end
  end
end

ActiveRecord::Base.send(:include, HasMaskedIdentifier)

Sorry for the lower-than-average quality explanation in this post (I only had about 30 min to pull it together). I’ll come back and improve on it when I turn this into a Rails plugin. That said, I wanted to get this post out there so others could see.