Skip to content

Commit

Permalink
feat: Allow setting a cool id on a different column (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
schpet authored Aug 25, 2024
1 parent 5941e0a commit c36b4a1
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 17 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,26 @@ and generate ids without creating a record
# generate an id, e.g. for batch inserts or upserts
User.generate_cool_id
# => "usr_vktd1b5v84lr"
```

you can use cool_id with a separate field, keeping the default primary key:

```ruby
class Product < ActiveRecord::Base
include CoolId::Model
cool_id prefix: "prd", id_field: :public_id
end

product = Product.create!(name: "Cool Product")
product.id # => 1 (or another integer)
product.public_id # => "prd_vktd1b5v84lr"

# You can still use CoolId.locate with the public_id
CoolId.locate("prd_vktd1b5v84lr") # => #<Product id: 1, public_id: "prd_vktd1b5v84lr", name: "Cool Product">
```

this approach allows you to keep your primary key as an auto-incrementing integer while still benefiting from CoolId's functionality. it's particularly useful when you want to expose a public identifier that's separate from your internal primary key.

it takes parameters to change the alphabet or length

```ruby
Expand Down
25 changes: 18 additions & 7 deletions lib/cool_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class MaxRetriesExceededError < StandardError; end
DEFAULT_LENGTH = 12
DEFAULT_MAX_RETRIES = 1000

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

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

def configure
yield self
Expand All @@ -29,6 +29,7 @@ def reset_configuration
self.alphabet = DEFAULT_ALPHABET
self.length = DEFAULT_LENGTH
self.max_retries = DEFAULT_MAX_RETRIES
self.id_field = nil
end

def registry
Expand All @@ -54,6 +55,10 @@ def generate_id(config)
end
end
end

def resolve_cool_id_field(model_class)
model_class.cool_id_config&.id_field || CoolId.id_field || model_class.primary_key
end
end

self.separator = DEFAULT_SEPARATOR
Expand All @@ -72,26 +77,31 @@ def register(prefix, model_class)

def locate(id)
parsed = parse(id)
parsed&.model_class&.find_by(id: id)
return nil unless parsed

id_field = CoolId.resolve_cool_id_field(parsed.model_class)
parsed.model_class.find_by(id_field => id)
end

def parse(id)
prefix, key = id.split(CoolId.separator, 2)
model_class = @prefix_map[prefix]
return nil unless model_class
Id.new(key, prefix, id, model_class)
id_field = CoolId.resolve_cool_id_field(model_class)
Id.new(key, prefix, id, model_class, id_field)
end
end

class Config
attr_reader :prefix, :length, :alphabet, :max_retries, :model_class
attr_reader :prefix, :length, :alphabet, :max_retries, :model_class, :id_field

def initialize(prefix:, model_class:, length: nil, alphabet: nil, max_retries: nil)
def initialize(prefix:, model_class:, length: nil, alphabet: nil, max_retries: nil, id_field: nil)
@length = length
@prefix = validate_prefix(prefix)
@alphabet = validate_alphabet(alphabet)
@max_retries = max_retries
@model_class = model_class
@id_field = id_field
end

private
Expand Down Expand Up @@ -148,7 +158,8 @@ def inherited(subclass)
private

def set_cool_id
self.id = self.class.generate_cool_id if id.blank?
id_field = CoolId.resolve_cool_id_field(self.class)
self[id_field] = self.class.generate_cool_id if self[id_field].blank?
end

def ensure_cool_id_configured
Expand Down
44 changes: 34 additions & 10 deletions spec/cool_id_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@

require "active_record"

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

class Customer < ActiveRecord::Base
include CoolId::Model
cool_id prefix: "cus", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 8, max_retries: 500
end

RSpec.describe CoolId do
before(:each) do
CoolId.reset_configuration
Expand Down Expand Up @@ -101,6 +91,16 @@ def self.exists?(id:)
end

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

class Customer < ActiveRecord::Base
include CoolId::Model
cool_id prefix: "cus", alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", length: 8, max_retries: 500
end

before(:all) do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
end
Expand Down Expand Up @@ -219,6 +219,30 @@ class LimitedRetryModel < ActiveRecord::Base
expect(CoolId.locate(user.id)).to eq(user)
expect(CoolId.locate(customer.id)).to eq(customer)
end

it "generates a cool_id for a custom id field" do
class Product < ActiveRecord::Base
include CoolId::Model
cool_id prefix: "prd", id_field: :public_id
end

ActiveRecord::Schema.define do
create_table :products do |t|
t.string :public_id
t.string :name
end
end

product = Product.create!(name: "Cool Product")
expect(product.id).to be_a(Integer)
expect(product.public_id).to match(/^prd_[0-9a-z]{12}$/)
expect(product.id).not_to match(/^prd_[0-9a-z]{12}$/)

located_product = CoolId.locate(product.public_id)
expect(located_product).to eq(product)

ActiveRecord::Base.connection.drop_table :products
end
end

describe "ensure_cool_id_setup behavior" do
Expand Down

0 comments on commit c36b4a1

Please sign in to comment.