Skip to content

dankozlowski/ashid-ruby

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ashid

Gem Version Test License: MIT

Time-sortable unique identifiers with type prefixes. Ruby port of the ashid library, wire-compatible with the TypeScript and Kotlin implementations.

Ashid.generate("user")  # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate("tx")    # => "tx_1je3kvrg1000075n93qdpk"
Ashid.generate          # => "1je3kvrg000007fn17wx6b"

Why ashid?

UUIDs are opaque. When you see 550e8400-e29b-41d4-a716-446655440000 in a log, you have no idea what it represents.

ashid generates IDs that tell you what they are:

user_1je3kvrg000007fn17wx6b   ← Obviously a user
tx_1je3kvrg1000075n93qdpk     ← Obviously a transaction
asset_1je3kvrg200005j6x7eygm  ← Obviously an asset

Features

  • Type prefixes — Self-documenting IDs, like Stripe (sk_, pi_, cus_).
  • Time-sortable — Lexicographic sort = chronological sort.
  • Crockford Base32 — Case-insensitive, I/L→1, O→0 (no more "is that a zero or an O?").
  • Double-click selectable — No hyphens or special characters.
  • URL-safe — No encoding required.
  • Zero runtime dependencies — Just stdlib SecureRandom.

Installation

gem install ashid

Or in a Gemfile:

gem "ashid"

Quick Start

require "ashid"

Ashid.generate                    # => "1je3kvrg000007fn17wx6b"
Ashid.generate("user")            # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate4("tok")            # => "tok_14d2pf2dbsqqg0zvebn63pags1" (random-only, like UUIDv4)

id = Ashid.generate("user")
Ashid.parse(id)                   # => {prefix: "user_", encoded_timestamp: "...", encoded_random: "..."}
Ashid.timestamp(id)               # => 1733140800000
Ashid.time(id)                    # => 2024-12-02 12:00:00 UTC
Ashid.valid?(id)                  # => true

API Reference

Ashid.generate(prefix = nil, time:, random:)

Generate a time-sortable ID, optionally with a prefix.

Ashid.generate                                  # => "1je3kvrg000007fn17wx6b"
Ashid.generate("user")                          # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate("user", time: 1_733_140_800_000, random: 8_234_567_890_123)
  • prefix (positional, optional): alphanumeric chars are kept and lowercased; everything else is stripped. The trailing _ delimiter is added automatically — passing "user", "user_", or "user-" yields the same result.
  • time: (keyword, defaults to current ms): Integer milliseconds since Unix epoch. Range: 0 to 35_184_372_088_831 (Dec 12, 3084).
  • random: (keyword, defaults to a secure 64-bit random Integer): non-negative Integer.

Raises ArgumentError for negative time, time exceeding the max, or negative random.

Ashid.generate4(prefix = nil, random1:, random2:)

Generate a random-only ID (UUIDv4 equivalent), without time-sortability. 26-char base, ~106 bits of entropy. Useful for tokens, secrets, or any case where unpredictability matters more than ordering.

Ashid.generate4         # => "14d2pf2dbsqqg0zvebn63pags1" (26 chars)
Ashid.generate4("tok")  # => "tok_14d2pf2dbsqqg0zvebn63pags1"

Ashid.parse(id)

Parse an ID into its components.

Ashid.parse("user_1je3kvrg000007fn17wx6b")
# => {prefix: "user_", encoded_timestamp: "1je3kvrg0", encoded_random: "00007fn17wx6b"}

Raises Ashid::InvalidIdError for nil, empty, or malformed input.

Ashid.prefix(id), Ashid.timestamp(id), Ashid.time(id), Ashid.random(id), Ashid.random_bytes(id)

Convenience accessors:

id = Ashid.generate("user")

Ashid.prefix(id)        # => "user_"
Ashid.timestamp(id)     # => 1733140800000   (Integer ms)
Ashid.time(id)          # => 2024-12-02 12:00:00 UTC  (Time object)
Ashid.random(id)        # => 8234567890123   (Integer)
Ashid.random_bytes(id)  # => "\x00\x00..."   (8-byte ASCII-8BIT String)

Ashid.valid?(id)

Returns true/false. Never raises, even on nil or non-String input.

Ashid.valid?("user_1je3kvrg000007fn17wx6b")  # => true
Ashid.valid?(nil)                            # => false
Ashid.valid?("garbage")                      # => false

Ashid.normalize(id)

Canonicalize an ID: lowercase the prefix and resolve Crockford lookalikes (I/L → 1, O → 0, U → V).

Ashid.normalize("USER_IJE3KVRGOOOOO7FNI7WX6B")
# => "user_1je3kvrg000007fn17wx6b"

Format

[prefix_]?[timestamp][random]
   ↓         ↓          ↓
 user_   1je3kvrg0  00007fn17wx6b
  • With prefix: variable-length timestamp + 13-char padded random.
  • Without prefix: 9-char zero-padded timestamp + 13-char padded random = fixed 22 chars.
  • generate4 (random-only): 13 + 13 = 26-char base.

Crockford alphabet: 0123456789abcdefghjkmnpqrstvwxyz. Excludes i, l, o, u. On decode, lookalikes map: I/L → 1, O → 0, U → V.

Cross-language compatibility

ashid IDs round-trip byte-for-byte across:

A test fixture in test/fixtures/parity.json verifies parity against IDs generated by the TS reference implementation.

Comparison

Feature ashid UUID nanoid ULID SecureRandom.uuid
Type prefixes Built-in No No No No
Time-sortable Yes No No Yes No
Human-readable encoding Crockford Base32 Hex Base64 Base32 Hex
Case-insensitive Yes Yes No No Yes
Lookalike correction Yes (I→1, O→0) No No No No
Double-click selectable Yes No (hyphens) Yes Yes No
URL-safe Yes Needs encoding Yes Yes Needs encoding
Stdlib only Yes (gem) (gem) (gem) Yes

Inspired by

Credits

Original ashid library by Dathan Guiley at Wilde Agency. Ruby port by Dan Kozlowski.

License

MIT — see LICENSE.txt.

About

About A better uuid - time-sortable unique identifiers with shorter 32bit encoding, optional type prefixes, Crockford alphabet for no similar characters, designed for double-click selection.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages