diff --git a/Gemfile.lock b/Gemfile.lock index 022670e..315705e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,15 +2,46 @@ PATH remote: . specs: cool_id (0.1.0) + activerecord (>= 6.0) + activesupport (>= 6.0) + nanoid (~> 2.0) GEM remote: https://rubygems.org/ specs: + activemodel (7.2.0) + activesupport (= 7.2.0) + activerecord (7.2.0) + activemodel (= 7.2.0) + activesupport (= 7.2.0) + timeout (>= 0.4.0) + activesupport (7.2.0) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.8) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) diff-lcs (1.5.1) + drb (2.2.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) json (2.7.2) language_server-protocol (3.17.0.3) lint_roller (1.1.0) + logger (1.6.0) + mini_portile2 (2.8.7) + minitest (5.25.0) + nanoid (2.0.0) parallel (1.26.2) parser (3.3.4.2) ast (~> 2.4.1) @@ -51,6 +82,10 @@ GEM rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) + securerandom (0.3.1) + sqlite3 (1.7.3) + mini_portile2 (~> 2.8.0) + sqlite3 (1.7.3-arm64-darwin) standard (1.40.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -64,6 +99,9 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.21.0) strscan (3.1.0) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) PLATFORMS @@ -74,6 +112,7 @@ DEPENDENCIES cool_id! rake (~> 13.0) rspec (~> 3.0) + sqlite3 (~> 1.4) standard (~> 1.3) BUNDLED WITH diff --git a/cool_id.gemspec b/cool_id.gemspec index b9d53d3..3dcf13b 100644 --- a/cool_id.gemspec +++ b/cool_id.gemspec @@ -32,8 +32,11 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency "nanoid", "~> 2.0" + spec.add_dependency "activerecord", ">= 6.0" + spec.add_dependency "activesupport", ">= 6.0" + + spec.add_development_dependency "sqlite3", "~> 1.4" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/lib/cool_id.rb b/lib/cool_id.rb index bfdd7e2..6e54775 100644 --- a/lib/cool_id.rb +++ b/lib/cool_id.rb @@ -1,8 +1,48 @@ # frozen_string_literal: true require_relative "cool_id/version" +require "nanoid" +require "active_support/concern" +require "active_record" module CoolId class Error < StandardError; end - # Your code goes here... + + # 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) + end + + module Model + extend ActiveSupport::Concern + + class_methods do + attr_accessor :cool_id_prefix, :cool_id_separator, :cool_id_alphabet, :cool_id_length + + 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 + ) + end + end + + included do + before_create :set_cool_id + + private + + def set_cool_id + self.id = self.class.generate_cool_id if id.blank? + end + end + end end diff --git a/spec/cool_id_spec.rb b/spec/cool_id_spec.rb index bd28915..6525525 100644 --- a/spec/cool_id_spec.rb +++ b/spec/cool_id_spec.rb @@ -1,11 +1,90 @@ # frozen_string_literal: true +require "active_record" + +# frozen_string_literal: true + +class User < ActiveRecord::Base + include CoolId::Model + self.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 +end + RSpec.describe CoolId do it "has a version number" do expect(CoolId::VERSION).not_to be nil end - it "does something useful" do - expect(false).to eq(true) + describe ".generate_id" do + it "generates an ID with default parameters" do + id = CoolId.generate_id + 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}$/) + end + + it "generates an ID without prefix when prefix is empty" do + id = CoolId.generate_id(prefix: "", length: 15) + 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) + expect(id).to match(/^[A-Z]{10}$/) + end + end + + describe CoolId::Model do + before(:all) do + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + end + + before(:each) do + ActiveRecord::Schema.define do + create_table :users, id: false do |t| + t.string :id, primary_key: true + t.string :name + end + + create_table :custom_users, id: false do |t| + t.string :id, primary_key: true + t.string :name + end + end + end + + after(:each) do + ActiveRecord::Base.connection.drop_table :users + ActiveRecord::Base.connection.drop_table :custom_users + end + + after(:all) do + ActiveRecord::Base.connection.close + end + + it "generates a cool_id for a new record" do + user = User.create(name: "John Doe") + expect(user.id).to match(/^usr_[0-9a-z]{12}$/) + end + + it "does not overwrite an existing id" do + user = User.create(id: "custom-id", name: "Jane Doe") + expect(user.id).to eq("custom-id") + end + + it "generates a cool_id with custom settings" do + custom_user = CustomUser.create(name: "Alice") + expect(custom_user.id).to match(/^cus-[A-Z]{8}$/) + end end end