Skip to content

Instantly share code, notes, and snippets.

@herenow
Last active November 25, 2023 09:12
Show Gist options
  • Save herenow/1b7e8ee41e73986067c537560bf16693 to your computer and use it in GitHub Desktop.
Save herenow/1b7e8ee41e73986067c537560bf16693 to your computer and use it in GitHub Desktop.
Stripe like id generation in Rails
# app/models/concerns/object_id.rb
module ObjectId
class ObjectIdReservedErr < StandardError; end
class ObjectIdPersistedErr < StandardError; end
def self.included(base)
base.extend ClassMethods
base.send :include, InstanceMethods
end
module ClassMethods
# :identification :: A friendly prefix for the object id, so we can
# easily identify the object type from its id (default: class name)
# :opts[field]:: The column to hold the id (default: :id)
def use_object_id(identification, opts = {})
id_prefix = identification.to_s || self.class.name.downcase.gsub('::', '_')
field = opts[:field] || :id
setter = :"#{field}="
unless method_defined?(setter)
raise(
"This object instance's does not respond_to #{setter.to_s}, " +
"we cannot use it as the as the object_id field."
)
end
@object_id_field = field
@object_id_setter = setter
@object_id_identification = reserve_object_id_identification(id_prefix)
# Add callback
send(:before_create, :generate_object_id!)
end
def reserve_object_id_identification(identification)
@@reserved_object_id_identification ||= {}
reserved_by = @@reserved_object_id_identification[identification]
if reserved_by.present? && reserved_by != self
raise ObjectIdReservedErr.new(
"[#{self.name}] can't use identification [#{identification}] already in use by [#{reserved_by}] class"
)
end
@@reserved_object_id_identification[identification] = self
identification
end
end
module InstanceMethods
def generate_object_id!
field = self.class.instance_variable_get(:@object_id_field)
setter = self.class.instance_variable_get(:@object_id_setter)
identification = self.class.instance_variable_get(:@object_id_identification)
raise ObjectIdPersistedErr.new(
"This object was already persisted, you can't regenerate the " +
"`#{field}` (object_id) column."
) if persisted?
object_id = ObjectIdGenerator.new(identification).to_s
send(setter, object_id)
end
end
end
# lib/object_id_generator.rb
class ObjectIdGenerator
BASE62_MAP = [
'0', '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', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z'
].freeze
BASE62_MAP_LENGTH = BASE62_MAP.length
raise "Unordered BASE62_MAP" if BASE62_MAP != BASE62_MAP.sort
attr_reader :id
def initialize(identification)
@prefix = identification
@id = "#{id_part}_#{rand_part}"
self
end
def to_s
@id
end
private
def rand_part
random(BASE62_MAP, 18)
end
def id_part
return unless @prefix
if @prefix.is_a? String
"#{@prefix.downcase}"
elsif @prefix.is_a? Class
"#{@prefix.name.downcase}"
else
"#{@prefix.class.name.downcase}"
end
end
def random(collection, chars)
length = collection.length
(0...chars).map do
collection[rand(length)]
end.join
end
end
# spec/lib/object_id_generator.rb
require 'spec_helper'
require 'object_id_generator'
RSpec.describe ObjectIdGenerator do
let(:identification) { 'some_prefix' }
let(:generator) { described_class.new(identification) }
describe '#to_s' do
it 'returns an object identification' do
expect(generator.to_s).to match(
/^some_prefix_[A-Za-z0-9_]{16}/
)
end
end
end
# spec/models/concerns/object_id.rb
require 'rails_helper'
RSpec.describe ObjectId do
class User < ActiveRecord::Base
include ObjectId
use_object_id :usr
end
describe 'ObjectID generation' do
context 'valid' do
it 'generated an object_id' do
user = User.new
user.generate_object_id!
expect(user.id).to match /^usr_/
end
end
context 'invalid' do
context 'already in use identification' do
it 'raises an ObjectIdReservedErr' do
expect {
class User2 < ActiveRecord::Base
include ObjectId
use_object_id :usr
end
}.to raise_error(ObjectId::ObjectIdReservedErr)
end
end
context 'persisted object' do
let(:persisted) { true }
it 'raises an ObjectIdPersistedErr' do
user = User.new
allow(user).to receive(:persisted?) { true }
expect {
user.generate_object_id!
}.to raise_error(ObjectId::ObjectIdPersistedErr)
end
end
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include ObjectId
use_object_id :usr
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment