Skip to content

Instantly share code, notes, and snippets.

@jdtornow

jdtornow/cfid.rb Secret

Created February 4, 2025 19:11
Show Gist options
  • Save jdtornow/334bcd526a8b1f2ed1f211f354e720cb to your computer and use it in GitHub Desktop.
Save jdtornow/334bcd526a8b1f2ed1f211f354e720cb to your computer and use it in GitHub Desktop.
Customer Facing/Friendly Identifiers for Rails
class Cfid
attr_reader :prefix
attr_reader :id
def initialize(prefix:, id:)
@prefix = prefix
@id = id
end
def encoded_id
sqids.encode([id])
end
def model
self.class.models[prefix]
end
def record
if model.present? && id.present?
model.find_by(id:)
end
end
def to_s
"#{ prefix }_#{ encoded_id }"
end
def self.alphabet
# Rails.application.credentials.dig(:cfid, :alphabet)
# - or -
# ENV["CFID_ALPHABET"]
#
# This is a hard-coded example for this explanation:
"GWkjSVLyXKsoJ1nziMNue8Rafqlgxb4IhQCBtr03HUYAd9v7mP2D6FE5wOpTcZ"
end
def self.find(cfid)
prefix, encoded_id = cfid.to_s.strip.split("_")
return nil unless prefix.present? && encoded_id.present?
ids = sqids.decode(encoded_id)
return nil unless ids.present?
new(prefix:, id: ids.last)
end
# A memoized list of models and prefix for easy lookup later
#
# If any prefixes are changed, make sure you restart your server or console
# to reload this value.
def self.models
return @models if defined?(@models)
@models = {}
Dir.glob(Rails.root.join("app/models/**/*.rb")).each do |path|
class_name = path.gsub(Rails.root.join("app/models/").to_s, "").gsub(/\.rb$/, "").gsub("concerns/", "").classify
klass = class_name.safe_constantize
next unless klass&.respond_to?(:cfid_prefix)
clean_prefix = klass.cfid_prefix.to_s.gsub(/_$/, "").to_s
next unless clean_prefix.present?
@models[clean_prefix] = klass
end
@models
end
def self.sqids
@sqids ||= Sqids.new(alphabet:, min_length: 8)
end
private
def sqids
self.class.sqids
end
end
module CustomerFacingIdentifiable
extend ActiveSupport::Concern
def cfid
return nil if new_record?
@cfid ||= Cfid.new(prefix: cfid_prefix, id:)
end
def cfid_prefix
self.class.cfid_prefix
end
def key
[ to_param ]
end
def to_cfid
cfid&.to_s
end
def to_param
to_cfid
end
class_methods do
def cfid_prefix=(prefix)
@cfid_prefix = prefix.to_s
end
def cfid_prefix
@cfid_prefix || default_prefix_for_cfid
end
def customer_facing_prefix(value)
self.cfid_prefix = value
end
# Fallback to a calculated prefix, if none is provided for this model.
#
# User => "us"
# UserAccount => "usac"
def default_prefix_for_cfid
self.model_name.name.underscore.split("_").map { |s| s[0..1] }.join("")
end
def find_by_cfid(cfid_str)
cfid = Cfid.find(cfid_str)
return nil unless cfid.present?
return nil unless cfid.prefix == cfid_prefix
cfid.record
end
def find_by_cfid!(cfid)
if record = find_by_cfid(cfid)
record
else
raise ActiveRecord::RecordNotFound
end
end
end
end
class Project < ApplicationRecord
include CustomerFacingIdentifiable
customer_facing_prefix :proj
end
class ProjectsController < ApplicationController
# GET /projects/proj_123
def show
@project = Project.find_by_cfid!(params[:id])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment