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.6.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.
-
.sortable_timestamp(str, format: :base62) ⇒ Time
Extract the embedded timestamp from a sortable ID.
-
.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
150 151 152 153 |
# File 'lib/philiprehberger/compact_id.rb', line 150 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
160 161 162 163 |
# File 'lib/philiprehberger/compact_id.rb', line 160 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
128 129 130 131 132 |
# File 'lib/philiprehberger/compact_id.rb', line 128 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
139 140 141 142 143 |
# File 'lib/philiprehberger/compact_id.rb', line 139 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
188 189 190 191 192 193 194 195 |
# File 'lib/philiprehberger/compact_id.rb', line 188 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.
206 207 208 209 210 211 212 213 214 215 |
# File 'lib/philiprehberger/compact_id.rb', line 206 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
169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/philiprehberger/compact_id.rb', line 169 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
224 225 226 227 228 |
# File 'lib/philiprehberger/compact_id.rb', line 224 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
236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/philiprehberger/compact_id.rb', line 236 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 |
.sortable_timestamp(str, format: :base62) ⇒ Time
Extract the embedded timestamp from a sortable ID.
Recovers the high 64 bits of the integer encoded by ‘sortable_id` (millisecond Unix epoch) and returns it as a `Time`.
109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/philiprehberger/compact_id.rb', line 109 def self.(str, format: :base62) raise Error, 'Expected a non-empty String' unless str.is_a?(String) && !str.empty? alphabet = case format when :base58 then BASE58_ALPHABET when :base62 then BASE62_ALPHABET else raise Error, "unsupported format: #{format}" end combined = decode_str(str, alphabet) ms = combined >> 64 Time.at(ms / 1000.0) 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
253 254 255 256 257 |
# File 'lib/philiprehberger/compact_id.rb', line 253 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
263 264 265 266 267 |
# File 'lib/philiprehberger/compact_id.rb', line 263 def self.valid_base62?(str) return false unless str.is_a?(String) && !str.empty? str.each_char.all? { |c| BASE62_ALPHABET.include?(c) } end |