Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions construct.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ import (
"fmt"
)

// Components describes the public fields packed into an OrderlyID.
type Components struct {
Prefix string
TimeMs int64
Flags uint8
Tenant uint16
Seq uint16
Shard uint16
// Prefix is the type prefix before the underscore separator.
Prefix string
// TimeMs is the absolute UTC timestamp in Unix milliseconds.
TimeMs int64
// Flags is the raw flags byte stored in the payload.
Flags uint8
// Tenant is the embedded 16-bit tenant identifier.
Tenant uint16
// Seq is the 12-bit sequence number packed into the payload.
Seq uint16
// Shard is the embedded 16-bit shard identifier.
Shard uint16
// Random60 is the low 60 bits of random payload data.
Random60 uint64
}

// NewFromParts builds an OrderlyID from explicit component values.
func NewFromParts(c Components, withChecksum bool) (string, error) {
if err := validatePrefix(c.Prefix); err != nil {
return "", err
Expand All @@ -40,7 +49,8 @@ func NewFromParts(c Components, withChecksum bool) (string, error) {
return base, nil
}

// NewFromPartsHex is a convenience that accepts random as big-endian hex string.
// NewFromPartsHex builds an OrderlyID from explicit component values and a
// big-endian random value encoded as hex.
func NewFromPartsHex(c Components, randomHex string, withChecksum bool) (string, error) {
rb, err := hex.DecodeString(randomHex)
if err != nil {
Expand Down
24 changes: 24 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Package orderlyid generates and parses typed, lexicographically sortable IDs.
//
// An OrderlyID has the canonical form "prefix_payload" or
// "prefix_payload-checksum". The prefix is a lowercase typed identifier such as
// "order" or "user" and must match [a-z][a-z0-9]{1,30}. The payload is a
// 32-character Crockford Base32 encoding of a packed 160-bit big-endian body.
//
// The binary body contains:
//
// - timestamp: 48-bit milliseconds since 2020-01-01T00:00:00Z, which makes IDs
// approximately ordered by creation time
// - flags: an 8-bit field where bits 7..6 carry the wire version, bit 5 marks
// privacy bucketing, and bits 4..0 are reserved
// - tenant: a 16-bit tenant identifier for multi-tenant systems
// - sequence: a 12-bit counter for bursts within the same millisecond
// - shard: a 16-bit routing hint, either provided directly or derived from
// bytes
// - random: 60 bits of cryptographic randomness
//
// Canonical output from this package is lowercase. Parsing is
// case-insensitive for payload and checksum characters, but prefixes must still
// satisfy the lowercase prefix rule. A trailing 4-character checksum is
// optional and can be used to detect copy/paste and transcription errors.
package orderlyid
32 changes: 25 additions & 7 deletions orderlyid.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@ type options struct {
bucketSeconds int
}

// Option configures ID generation in New.
type Option func(*options)

// WithTenant sets the 16-bit tenant value embedded in generated IDs.
func WithTenant(t uint16) Option {
return func(o *options) {
o.tenant = t
}
}

// WithShard sets the 16-bit shard value embedded in generated IDs.
func WithShard(s uint16) Option {
return func(o *options) {
o.shard = s
}
}

// WithShardFromBytes hashes b into a deterministic 16-bit shard value.
func WithShardFromBytes(b []byte) Option {
return func(o *options) {
var h uint32
Expand All @@ -42,12 +46,14 @@ func WithShardFromBytes(b []byte) Option {
}
}

// WithChecksum enables or disables the trailing 4-character checksum.
func WithChecksum(v bool) Option {
return func(o *options) {
o.withChecksum = v
}
}

// WithBucketSeconds rounds the embedded timestamp down to sec-second buckets.
func WithBucketSeconds(sec int) Option {
return func(o *options) {
o.bucketSeconds = sec
Expand Down Expand Up @@ -98,7 +104,11 @@ var (
seq12 uint16 // 12-bit
)

// New generates a new OrderlyID string like "order_0r8h...".
// New generates a new OrderlyID such as "order_0r8h...".
//
// The prefix must match the public ID type naming rules used by Parse. New
// panics if the prefix is invalid or if cryptographic randomness cannot be
// read.
func New(prefix string, opts ...Option) string {
if !prefixRe.MatchString(prefix) {
panic("invalid prefix")
Expand Down Expand Up @@ -154,17 +164,25 @@ func New(prefix string, opts ...Option) string {
return id
}

// Parse decodes an OrderlyID and returns its components.
// Parsed is the decoded representation of an OrderlyID.
type Parsed struct {
// Prefix is the type prefix before the underscore separator.
Prefix string
TimeMs int64 // epoch ms (UTC)
Flags byte
// TimeMs is the absolute UTC timestamp in Unix milliseconds.
TimeMs int64
// Flags is the raw flags byte stored in the payload.
Flags byte
// Tenant is the embedded 16-bit tenant identifier.
Tenant uint16
Seq uint16 // 12-bit
Shard uint16
Random uint64 // 60-bit
// Seq is the 12-bit per-millisecond sequence number.
Seq uint16
// Shard is the embedded 16-bit shard identifier.
Shard uint16
// Random is the 60-bit random suffix stored in the payload.
Random uint64
}

// Parse decodes an OrderlyID string and returns its components.
func Parse(s string) (*Parsed, error) {
s = strings.TrimSpace(s)
base := s
Expand Down
Loading