Skip to content

Commit

Permalink
feat: Add a registry, cleanup configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
schpet committed Aug 16, 2024
1 parent 0f6879f commit f91a394
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 27 deletions.
102 changes: 86 additions & 16 deletions lib/cool_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,92 @@
module CoolId
class Error < StandardError; end

# defaults copped from
# https://planetscale.com/blog/why-we-chose-nanoids-for-planetscales-api
DEFAULT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
DEFAULT_SEPARATOR = "_"
DEFAULT_LENGTH = 12

def self.generate_id(prefix: "", separator: DEFAULT_SEPARATOR, length: DEFAULT_LENGTH, alphabet: DEFAULT_ALPHABET)
id = Nanoid.generate(size: length, alphabet: alphabet)
[prefix, id].reject(&:empty?).join(separator)
class << self
attr_accessor :separator

def configure
yield self
end

def registry
@registry ||= Registry.new
end

def generate_id(config)
id = Nanoid.generate(size: config.length, alphabet: config.alphabet)
[config.prefix, id].reject(&:empty?).join(@separator)
end
end

self.separator = "_"

class Registry
def initialize
@registry = {}
end

def register(prefix, model_class)
@registry[prefix] = model_class
end

def find_model(prefix)
@registry[prefix]
end

def find_record(id)
prefix, _ = id.split(CoolId.separator, 2)
model_class = find_model(prefix)
model_class&.find_by(id: id)
end

def find_record!(id)
prefix, _ = id.split(CoolId.separator, 2)
model_class = find_model(prefix)
model_class&.find(id)
end
end

class Config
attr_reader :prefix, :length, :alphabet

def initialize(prefix: "", length: 12, alphabet: "0123456789abcdefghijklmnopqrstuvwxyz")
@prefix = prefix
@length = length
self.alphabet = alphabet
end

def alphabet=(value)
validate_alphabet(value)
@alphabet = value
end

private

def validate_alphabet(value)
if value.include?(CoolId.separator)
raise ArgumentError, "Alphabet cannot include the separator '#{CoolId.separator}'"
end
end
end

module Model
extend ActiveSupport::Concern

class_methods do
attr_accessor :cool_id_prefix, :cool_id_separator, :cool_id_alphabet, :cool_id_length
attr_reader :cool_id_config

def cool_id(options = {})
register_cool_id(options)
end

def register_cool_id(options = {})
raise ArgumentError, "Prefix cannot be empty" if options[:prefix] && options[:prefix].empty?
@cool_id_config = Config.new(**options)
CoolId.registry.register(options[:prefix], self)
end

def generate_cool_id
CoolId.generate_id(
prefix: cool_id_prefix,
separator: cool_id_separator || DEFAULT_SEPARATOR,
length: cool_id_length || DEFAULT_LENGTH,
alphabet: cool_id_alphabet || DEFAULT_ALPHABET
)
CoolId.generate_id(@cool_id_config || Config.new)
end
end

Expand All @@ -45,4 +107,12 @@ def set_cool_id
end
end
end

def self.find(id)
registry.find_record(id)
end

def self.find!(id)
registry.find_record!(id)
end
end
88 changes: 77 additions & 11 deletions spec/cool_id_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@

class User < ActiveRecord::Base
include CoolId::Model
self.cool_id_prefix = "usr"
register_cool_id prefix: "usr"
end

class CustomUser < ActiveRecord::Base
include CoolId::Model
self.cool_id_prefix = "cus"
self.cool_id_separator = "-"
self.cool_id_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
self.cool_id_length = 8
register_cool_id prefix: "cus", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 8
end

RSpec.describe CoolId do
Expand All @@ -24,24 +21,36 @@ class CustomUser < ActiveRecord::Base

describe ".generate_id" do
it "generates an ID with default parameters" do
id = CoolId.generate_id
config = CoolId::Config.new
id = CoolId.generate_id(config)
expect(id).to match(/^[0-9a-z]{12}$/)
end

it "generates an ID with custom prefix, separator, and length" do
id = CoolId.generate_id(prefix: "test", separator: "-", length: 10)
expect(id).to match(/^test-[0-9a-z]{10}$/)
it "generates an ID with custom prefix and length" do
config = CoolId::Config.new(prefix: "test", length: 10)
id = CoolId.generate_id(config)
expect(id).to match(/^test_[0-9a-z]{10}$/)
end

it "generates an ID without prefix when prefix is empty" do
id = CoolId.generate_id(prefix: "", length: 15)
config = CoolId::Config.new(prefix: "", length: 15)
id = CoolId.generate_id(config)
expect(id).to match(/^[0-9a-z]{15}$/)
end

it "generates an ID with custom alphabet" do
id = CoolId.generate_id(alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 10)
config = CoolId::Config.new(alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 10)
id = CoolId.generate_id(config)
expect(id).to match(/^[A-Z]{10}$/)
end

it "uses the globally configured separator" do
CoolId.configure { |config| config.separator = "-" }
config = CoolId::Config.new(prefix: "test", length: 10)
id = CoolId.generate_id(config)
expect(id).to match(/^test-[0-9a-z]{10}$/)
CoolId.separator = "_" # Reset to default
end
end

describe CoolId::Model do
Expand Down Expand Up @@ -83,8 +92,65 @@ class CustomUser < ActiveRecord::Base
end

it "generates a cool_id with custom settings" do
original_separator = CoolId.separator
CoolId.separator = "-"
custom_user = CustomUser.create(name: "Alice")
expect(custom_user.id).to match(/^cus-[A-Z]{8}$/)
CoolId.separator = original_separator
end

it "raises an error when trying to set an empty prefix" do
expect {
Class.new(ActiveRecord::Base) do
include CoolId::Model
register_cool_id prefix: ""
end
}.to raise_error(ArgumentError, "Prefix cannot be empty")
end

it "raises an error when the alphabet includes the separator" do
original_separator = CoolId.separator
CoolId.separator = "-"
expect {
CoolId::Config.new(alphabet: "ABC-DEF")
}.to raise_error(ArgumentError, "Alphabet cannot include the separator '-'")
CoolId.separator = original_separator
end

it "can find a record using CoolId.find" do
user = User.create(name: "John Doe")
found_user = CoolId.find(user.id)
expect(found_user).to eq(user)
end

it "can find a custom record using CoolId.find" do
custom_user = CustomUser.create(name: "Alice")
found_custom_user = CoolId.find(custom_user.id)
expect(found_custom_user).to eq(custom_user)
end

it "returns nil when trying to find a non-existent record" do
expect(CoolId.find("usr_nonexistent")).to be_nil
end

it "raises ActiveRecord::RecordNotFound when trying to find! a non-existent record" do
expect { CoolId.find!("usr_nonexistent") }.to raise_error(ActiveRecord::RecordNotFound)
end

it "returns nil when trying to find a record with an unknown prefix" do
expect(CoolId.find("unknown_prefix_123")).to be_nil
end

it "returns nil when trying to find! a record with an unknown prefix" do
expect(CoolId.find!("unknown_prefix_123")).to be_nil
end

it "works with different separators" do
user = User.create(name: "John Doe")
custom_user = CustomUser.create(name: "Jane Doe")

expect(CoolId.find(user.id)).to eq(user)
expect(CoolId.find(custom_user.id)).to eq(custom_user)
end
end
end

0 comments on commit f91a394

Please sign in to comment.