Files
platform/uid/uid.go
T
2026-06-28 17:52:49 +02:00

131 lines
2.8 KiB
Go

// Package uid provides a compact 64-bit identifier with base62 string form.
//
// The scheme is used consistently across commerce services (cart, order,
// profile, actor grains) so ids are URL/cookie-friendly and comparable
// across system boundaries. Each domain re-exports the type as its own
// named alias (e.g. order.OrderId = uid.ID) and keeps thin wrapper
// functions so call-site code never needs to import platform/uid directly.
package uid
import (
"crypto/rand"
"encoding/json"
"fmt"
)
// ID is a 64-bit identifier encoded as base62 text (max 11 chars).
type ID uint64
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var base62Rev [256]byte
func init() {
for i := range base62Rev {
base62Rev[i] = 0xFF
}
for i := 0; i < len(base62Alphabet); i++ {
base62Rev[base62Alphabet[i]] = byte(i)
}
}
// String returns the canonical base62 encoding.
func (id ID) String() string { return encodeBase62(uint64(id)) }
// MarshalJSON encodes the id as a JSON string.
func (id ID) MarshalJSON() ([]byte, error) {
return json.Marshal(id.String())
}
// UnmarshalJSON decodes the id from a JSON string containing base62 text.
func (id *ID) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, ok := Parse(s)
if !ok {
return fmt.Errorf("invalid uid: %q", s)
}
*id = parsed
return nil
}
// New generates a cryptographically random non-zero 64-bit id.
func New() (ID, error) {
var b [8]byte
if _, err := rand.Read(b[:]); err != nil {
return 0, fmt.Errorf("uid.New: %w", err)
}
u := (uint64(b[0]) << 56) |
(uint64(b[1]) << 48) |
(uint64(b[2]) << 40) |
(uint64(b[3]) << 32) |
(uint64(b[4]) << 24) |
(uint64(b[5]) << 16) |
(uint64(b[6]) << 8) |
uint64(b[7])
if u == 0 {
return New()
}
return ID(u), nil
}
// MustNew panics if generation fails.
func MustNew() ID {
id, err := New()
if err != nil {
panic(err)
}
return id
}
// Parse parses a base62 string into an ID. Returns (0, false) for invalid input.
func Parse(s string) (ID, bool) {
if len(s) == 0 || len(s) > 11 {
return 0, false
}
u, ok := decodeBase62(s)
if !ok {
return 0, false
}
return ID(u), true
}
// MustParse panics on invalid base62 input.
func MustParse(s string) ID {
id, ok := Parse(s)
if !ok {
panic(fmt.Sprintf("invalid uid: %q", s))
}
return id
}
// encodeBase62 converts a uint64 to base62 (shortest form, no leading zeros).
func encodeBase62(u uint64) string {
if u == 0 {
return "0"
}
var buf [11]byte
i := len(buf)
for u > 0 {
i--
buf[i] = base62Alphabet[u%62]
u /= 62
}
return string(buf[i:])
}
// decodeBase62 converts base62 text to uint64.
func decodeBase62(s string) (uint64, bool) {
var v uint64
for i := 0; i < len(s); i++ {
d := base62Rev[s[i]]
if d == 0xFF {
return 0, false
}
v = v*62 + uint64(d)
}
return v, true
}