Last active
November 25, 2023 09:12
-
-
Save herenow/1b7e8ee41e73986067c537560bf16693 to your computer and use it in GitHub Desktop.
Stripe like id generation in Rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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