Module: Philiprehberger::CompactId
- Defined in:
- lib/philiprehberger/compact_id.rb,
lib/philiprehberger/compact_id/version.rb
Defined Under Namespace
Classes: Error
Constant Summary collapse
- BASE58_ALPHABET =
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'- BASE62_ALPHABET =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'- UUID_PATTERN =
/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i- PREFIX_PATTERN =
/\A[a-zA-Z0-9]+\z/- VERSION =
'0.5.0'
Class Method Summary collapse
-
.base58_to_base62(str) ⇒ String
Convert a Base58 string directly to Base62.
-
.base62_to_base58(str) ⇒ String
Convert a Base62 string directly to Base58.
-
.batch_generate(count, format: :base58) ⇒ Array<String>
Generate multiple compact IDs at once.
-
.batch_to_base58(uuids) ⇒ Array<String>
Bulk encode an array of UUIDs to Base58.
-
.batch_to_base62(uuids) ⇒ Array<String>
Bulk encode an array of UUIDs to Base62.
-
.decode(str) ⇒ String
Auto-detect format and decode to UUID.
-
.decode_safe(str, expected_format:) ⇒ String
Safer variant of ‘.decode` that only succeeds when the detected encoding matches the expected format.
-
.format?(str) ⇒ Symbol
Detect the format of an encoded string.
-
.from_base58(str) ⇒ String
Decode a Base58 string back to a UUID.
-
.from_base62(str) ⇒ String
Decode a Base62 string back to a UUID.
-
.generate(format = :base58) ⇒ String
Generate a new UUID and encode it.
-
.generate_prefixed(prefix, format: :base58, separator: '_') ⇒ String
Generate a compact ID with a type prefix.
-
.parse_prefixed(str, separator: '_') ⇒ Hash
Parse a prefixed compact ID into its components.
-
.sortable_id(format: :base62) ⇒ String
Generate a time-sortable ID by combining a millisecond timestamp with random bytes.
-
.to_base58(uuid) ⇒ String
Encode a UUID as a Base58 string.
-
.to_base62(uuid) ⇒ String
Encode a UUID as a Base62 string.
-
.valid_base58?(str) ⇒ Boolean
Check if a string is valid Base58.
-
.valid_base62?(str) ⇒ Boolean
Check if a string is valid Base62.
Class Method Details
.base58_to_base62(str) ⇒ String
Convert a Base58 string directly to Base62
127 128 129 130 |
# File 'lib/philiprehberger/compact_id.rb', line 127 def self.base58_to_base62(str) num = decode_str(str, BASE58_ALPHABET) encode(num, BASE62_ALPHABET, 22) end |
.base62_to_base58(str) ⇒ String
Convert a Base62 string directly to Base58
137 138 139 140 |
# File 'lib/philiprehberger/compact_id.rb', line 137 def self.base62_to_base58(str) num = decode_str(str, BASE62_ALPHABET) encode(num, BASE58_ALPHABET, 22) end |
.batch_generate(count, format: :base58) ⇒ Array<String>
Generate multiple compact IDs at once
73 74 75 76 77 |
# File 'lib/philiprehberger/compact_id.rb', line 73 def self.batch_generate(count, format: :base58) raise Error, 'Count must be a positive integer' unless count.is_a?(Integer) && count.positive? Array.new(count) { generate(format) } end |
.batch_to_base58(uuids) ⇒ Array<String>
Bulk encode an array of UUIDs to Base58
105 106 107 108 109 |
# File 'lib/philiprehberger/compact_id.rb', line 105 def self.batch_to_base58(uuids) raise Error, 'Expected an Array of UUIDs' unless uuids.is_a?(Array) uuids.map { |uuid| to_base58(uuid) } end |
.batch_to_base62(uuids) ⇒ Array<String>
Bulk encode an array of UUIDs to Base62
116 117 118 119 120 |
# File 'lib/philiprehberger/compact_id.rb', line 116 def self.batch_to_base62(uuids) raise Error, 'Expected an Array of UUIDs' unless uuids.is_a?(Array) uuids.map { |uuid| to_base62(uuid) } end |
.decode(str) ⇒ String
Auto-detect format and decode to UUID
165 166 167 168 169 170 171 172 |
# File 'lib/philiprehberger/compact_id.rb', line 165 def self.decode(str) detected = format?(str) case detected when :base58 then from_base58(str) when :base62 then from_base62(str) else raise Error, "Unable to detect format of: #{str}" end end |
.decode_safe(str, expected_format:) ⇒ String
Safer variant of ‘.decode` that only succeeds when the detected encoding matches the expected format. Use when consuming IDs that should be a specific encoding to prevent silent confusion between Base58 and Base62.
183 184 185 186 187 188 189 190 191 192 |
# File 'lib/philiprehberger/compact_id.rb', line 183 def self.decode_safe(str, expected_format:) unless %i[base58 base62].include?(expected_format) raise ArgumentError, "unknown expected_format: #{expected_format.inspect}" end detected = format?(str) raise Error, "expected #{expected_format} but got #{detected}" unless detected == expected_format expected_format == :base58 ? from_base58(str) : from_base62(str) end |
.format?(str) ⇒ Symbol
Detect the format of an encoded string
146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/philiprehberger/compact_id.rb', line 146 def self.format?(str) return :unknown unless str.is_a?(String) && !str.empty? # Base58 is a strict subset of Base62, so check Base58 first. # A string is only :base62 if it contains characters outside the Base58 alphabet. if valid_base58?(str) :base58 elsif valid_base62?(str) :base62 else :unknown end end |
.from_base58(str) ⇒ String
Decode a Base58 string back to a UUID
39 40 41 |
# File 'lib/philiprehberger/compact_id.rb', line 39 def self.from_base58(str) int_to_uuid(decode_str(str, BASE58_ALPHABET)) end |
.from_base62(str) ⇒ String
Decode a Base62 string back to a UUID
48 49 50 |
# File 'lib/philiprehberger/compact_id.rb', line 48 def self.from_base62(str) int_to_uuid(decode_str(str, BASE62_ALPHABET)) end |
.generate(format = :base58) ⇒ String
Generate a new UUID and encode it
57 58 59 60 61 62 63 64 65 |
# File 'lib/philiprehberger/compact_id.rb', line 57 def self.generate(format = :base58) uuid = SecureRandom.uuid case format when :base58 then to_base58(uuid) when :base62 then to_base62(uuid) else raise Error, "Unknown format: #{format}. Use :base58 or :base62" end end |
.generate_prefixed(prefix, format: :base58, separator: '_') ⇒ String
Generate a compact ID with a type prefix
201 202 203 204 205 |
# File 'lib/philiprehberger/compact_id.rb', line 201 def self.generate_prefixed(prefix, format: :base58, separator: '_') validate_prefix!(prefix) validate_separator!(separator) "#{prefix}#{separator}#{generate(format)}" end |
.parse_prefixed(str, separator: '_') ⇒ Hash
Parse a prefixed compact ID into its components
213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/philiprehberger/compact_id.rb', line 213 def self.parse_prefixed(str, separator: '_') raise Error, 'Expected a non-empty String' unless str.is_a?(String) && !str.empty? parts = str.split(separator, 2) raise Error, "No separator '#{separator}' found in: #{str}" if parts.length < 2 || parts[1].empty? prefix = parts[0] id = parts[1] uuid = decode(id) { prefix: prefix, id: id, uuid: uuid } end |
.sortable_id(format: :base62) ⇒ String
Generate a time-sortable ID by combining a millisecond timestamp with random bytes
IDs generated later will always sort lexicographically after earlier ones. The ID is composed of the current time in milliseconds (high bits) and 64 bits of randomness (low bits), encoded in the specified format.
88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/philiprehberger/compact_id.rb', line 88 def self.sortable_id(format: :base62) ms = (Time.now.to_f * 1000).to_i random = SecureRandom.random_number(2**64) combined = (ms << 64) | random case format when :base58 then encode(combined, BASE58_ALPHABET, 0) when :base62 then encode(combined, BASE62_ALPHABET, 0) else raise Error, "unsupported format: #{format}" end end |
.to_base58(uuid) ⇒ String
Encode a UUID as a Base58 string
19 20 21 22 |
# File 'lib/philiprehberger/compact_id.rb', line 19 def self.to_base58(uuid) validate_uuid!(uuid) encode(uuid_to_int(uuid), BASE58_ALPHABET, 22) end |
.to_base62(uuid) ⇒ String
Encode a UUID as a Base62 string
29 30 31 32 |
# File 'lib/philiprehberger/compact_id.rb', line 29 def self.to_base62(uuid) validate_uuid!(uuid) encode(uuid_to_int(uuid), BASE62_ALPHABET, 22) end |
.valid_base58?(str) ⇒ Boolean
Check if a string is valid Base58
230 231 232 233 234 |
# File 'lib/philiprehberger/compact_id.rb', line 230 def self.valid_base58?(str) return false unless str.is_a?(String) && !str.empty? str.each_char.all? { |c| BASE58_ALPHABET.include?(c) } end |
.valid_base62?(str) ⇒ Boolean
Check if a string is valid Base62
240 241 242 243 244 |
# File 'lib/philiprehberger/compact_id.rb', line 240 def self.valid_base62?(str) return false unless str.is_a?(String) && !str.empty? str.each_char.all? { |c| BASE62_ALPHABET.include?(c) } end |