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.