Skip to content

Commit

Permalink
feat: Global config (#1)
Browse files Browse the repository at this point in the history
and api updates
  • Loading branch information
schpet authored Aug 17, 2024
1 parent f91a394 commit 9987737
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 79 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
cool_id (0.1.0)
cool_id (0.1.1)
activerecord (>= 6.0)
activesupport (>= 6.0)
nanoid (~> 2.0)
Expand Down
2 changes: 1 addition & 1 deletion cool_id.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
spec.homepage = "https://github.com/schpet/cool_id"
spec.required_ruby_version = ">= 3.0.0"

spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/schpet/cool_id"
Expand Down
87 changes: 48 additions & 39 deletions lib/cool_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,46 @@
require_relative "cool_id/version"
require "nanoid"
require "active_support/concern"
require "active_record"

module CoolId
class Error < StandardError; end

DEFAULT_SEPARATOR = "_"
DEFAULT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
DEFAULT_LENGTH = 12

Id = Struct.new(:key, :prefix, :id, :model_class)

class << self
attr_accessor :separator
attr_accessor :separator, :alphabet, :length

def configure
yield self
end

def reset_configuration
self.separator = DEFAULT_SEPARATOR
self.alphabet = DEFAULT_ALPHABET
self.length = DEFAULT_LENGTH
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)
alphabet = config.alphabet || @alphabet
length = config.length || @length
id = Nanoid.generate(size: length, alphabet: alphabet)

"#{config.prefix}#{separator}#{id}"
end
end

self.separator = "_"
# defaults based on https://planetscale.com/blog/why-we-chose-nanoids-for-planetscales-api
self.separator = DEFAULT_SEPARATOR
self.alphabet = DEFAULT_ALPHABET
self.length = DEFAULT_LENGTH

class Registry
def initialize
Expand All @@ -36,43 +53,40 @@ 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)
def locate(id)
parsed = parse(id)
parsed&.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)
def parse(id)
prefix, key = id.split(CoolId.separator, 2)
model_class = @registry[prefix]
return nil unless model_class
Id.new(key, prefix, id, model_class)
end
end

class Config
attr_reader :prefix, :length, :alphabet

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

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

private

def validate_prefix(value)
raise ArgumentError, "Prefix cannot be nil" if value.nil?
raise ArgumentError, "Prefix cannot consist only of whitespace" if value.strip.empty?
value
end

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

Expand All @@ -82,18 +96,13 @@ module Model
class_methods do
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?
def cool_id(options)
@cool_id_config = Config.new(**options)
CoolId.registry.register(options[:prefix], self)
end

def generate_cool_id
CoolId.generate_id(@cool_id_config || Config.new)
CoolId.generate_id(@cool_id_config)
end
end

Expand All @@ -108,11 +117,11 @@ def set_cool_id
end
end

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

def self.find!(id)
registry.find_record!(id)
def self.parse(id)
registry.parse(id)
end
end
2 changes: 1 addition & 1 deletion lib/cool_id/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module CoolId
VERSION = "0.1.0"
VERSION = "0.1.1"
end
115 changes: 78 additions & 37 deletions spec/cool_id_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,34 @@

class User < ActiveRecord::Base
include CoolId::Model
register_cool_id prefix: "usr"
cool_id prefix: "usr"
end

class CustomUser < ActiveRecord::Base
include CoolId::Model
register_cool_id prefix: "cus", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 8
cool_id prefix: "cus", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 8
end

RSpec.describe CoolId do
before(:each) do
CoolId.reset_configuration
end

it "has a version number" do
expect(CoolId::VERSION).not_to be nil
end

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

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

it "generates an ID with custom prefix and length" do
Expand All @@ -33,23 +43,50 @@ class CustomUser < ActiveRecord::Base
end

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

it "generates an ID with custom alphabet" do
config = CoolId::Config.new(alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 10)
config = CoolId::Config.new(prefix: "X", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 10)
id = CoolId.generate_id(config)
expect(id).to match(/^[A-Z]{10}$/)
expect(id).to match(/^X_[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

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

it "uses the config length over the global length" do
CoolId.configure { |config| config.length = 8 }
config = CoolId::Config.new(prefix: "test", length: 6)
id = CoolId.generate_id(config)
expect(id).to match(/^test_[0-9a-z]{6}$/)
end

it "resets configuration to default values" do
CoolId.configure do |config|
config.separator = "-"
config.alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
config.length = 8
end

CoolId.reset_configuration

expect(CoolId.separator).to eq(CoolId::DEFAULT_SEPARATOR)
expect(CoolId.alphabet).to eq(CoolId::DEFAULT_ALPHABET)
expect(CoolId.length).to eq(CoolId::DEFAULT_LENGTH)
end
end

Expand Down Expand Up @@ -92,65 +129,69 @@ 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
CoolId.reset_configuration
end

it "raises an error when trying to set an empty prefix" do
it "raises an error when trying to set an empty or whitespace-only prefix" do
expect {
Class.new(ActiveRecord::Base) do
include CoolId::Model
cool_id prefix: ""
end
}.to raise_error(ArgumentError, "Prefix cannot consist only of whitespace")

expect {
Class.new(ActiveRecord::Base) do
include CoolId::Model
register_cool_id prefix: ""
cool_id prefix: " "
end
}.to raise_error(ArgumentError, "Prefix cannot be empty")
}.to raise_error(ArgumentError, "Prefix cannot consist only of whitespace")

expect {
Class.new(ActiveRecord::Base) do
include CoolId::Model
cool_id prefix: nil
end
}.to raise_error(ArgumentError, "Prefix cannot be nil")
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")
CoolId::Config.new(prefix: "test", alphabet: "ABC-DEF")
}.to raise_error(ArgumentError, "Alphabet cannot include the separator '-'")
CoolId.separator = original_separator
CoolId.reset_configuration
end

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

it "can find a custom record using CoolId.find" do
it "can locate a custom record using CoolId.locate" 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)
located_custom_user = CoolId.locate(custom_user.id)
expect(located_custom_user).to eq(custom_user)
end

it "returns nil when trying to find a record with an unknown prefix" do
expect(CoolId.find("unknown_prefix_123")).to be_nil
it "returns nil when trying to locate a non-existent record" do
expect(CoolId.locate("usr_nonexistent")).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
it "returns nil when trying to locate a record with an unknown prefix" do
expect(CoolId.locate("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)
expect(CoolId.locate(user.id)).to eq(user)
expect(CoolId.locate(custom_user.id)).to eq(custom_user)
end
end
end

0 comments on commit 9987737

Please sign in to comment.