Skip to content
This repository was archived by the owner on Apr 29, 2026. It is now read-only.
63 changes: 36 additions & 27 deletions anyint64.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,87 +11,96 @@ import (

// AnyInt64 is a compact identifier with a runtime-configurable prefix.
// Unlike [Int64], the prefix is not fixed at compile time.
type AnyInt64 struct {
//
// Use [AnyPrefix] as P for unconstrained prefixes, or a custom enum type
// implementing [VariablePrefixer] for a known set of variants.
type AnyInt64[P Prefixer] struct {
prefix P
val int64
prefix string
}

func NewAnyInt64(prefix string) (AnyInt64, error) {
func NewAnyInt64[P Prefixer](p P) (AnyInt64[P], error) {
ms := time.Now().UnixMilli()

var rb [2]byte
if _, err := rand.Read(rb[:]); err != nil {
return AnyInt64{}, fmt.Errorf("typeid: crypto/rand: %w", err)
return AnyInt64[P]{}, fmt.Errorf("typeid: crypto/rand: %w", err)
}
r := int64(binary.BigEndian.Uint16(rb[:]) & 0x7FFF)

return AnyInt64{val: (ms << randomBits) | r, prefix: prefix}, nil
return AnyInt64[P]{val: (ms << randomBits) | r, prefix: p}, nil
}

func AnyInt64From(prefix string, v int64) (AnyInt64, error) {
func AnyInt64From[P Prefixer](p P, v int64) (AnyInt64[P], error) {
if v <= 0 {
return AnyInt64{}, ErrNonPositiveInt
return AnyInt64[P]{}, ErrNonPositiveInt
}
return AnyInt64{val: v, prefix: prefix}, nil
return AnyInt64[P]{val: v, prefix: p}, nil
}

func ParseAnyInt64(s string) (AnyInt64, error) {
func ParseAnyInt64[P Prefixer](s string) (AnyInt64[P], error) {
var p P
j := strings.LastIndex(s, "_") + 1
pref, suffix := s[:max(0, j-1)], s[j:]
if len(suffix) != int64SuffixLen {
return AnyInt64{}, fmt.Errorf("typeid: invalid format: %q", s)
return AnyInt64[P]{}, fmt.Errorf("typeid: invalid format: %q", s)
}
if vp, ok := any(&p).(VariablePrefixer); ok {
vp.ParsePrefix(pref)
}
if p.Prefix() != pref {
return AnyInt64[P]{}, fmt.Errorf("typeid: invalid prefix: %q", pref)
}
v, err := decodeBase32Int64(suffix)
if err != nil {
return AnyInt64{}, err
return AnyInt64[P]{}, err
}
if v <= 0 {
return AnyInt64{}, ErrNonPositiveInt
return AnyInt64[P]{}, ErrNonPositiveInt
}
return AnyInt64{val: v, prefix: pref}, nil
return AnyInt64[P]{val: v, prefix: p}, nil
}

func (id AnyInt64) Int64() int64 { return id.val }
func (id AnyInt64) Prefix() string { return id.prefix }
func (id *AnyInt64) SetPrefix(s string) {
id.prefix = s
}
func (id AnyInt64[P]) Int64() int64 { return id.val }
func (id AnyInt64[P]) Prefix() string { return id.prefix.Prefix() }
func (id AnyInt64[P]) Variant() P { return id.prefix }
func (id *AnyInt64[P]) SetPrefix(p P) { id.prefix = p }

func (id AnyInt64) appendText(dst []byte) []byte {
return appendBase32Int64(dst, id.prefix, id.val)
func (id AnyInt64[P]) appendText(dst []byte) []byte {
return appendBase32Int64(dst, id.prefix.Prefix(), id.val)
}

func (id AnyInt64) String() string {
func (id AnyInt64[P]) String() string {
var buf [64]byte
return string(id.appendText(buf[:0]))
}

func (id AnyInt64) IsZero() bool { return id.val == 0 }
func (id AnyInt64[P]) IsZero() bool { return id.val == 0 }

func (id AnyInt64) MarshalText() ([]byte, error) {
func (id AnyInt64[P]) MarshalText() ([]byte, error) {
if id.val <= 0 {
return nil, ErrNonPositiveInt
}
return id.appendText(nil), nil
}

func (id *AnyInt64) UnmarshalText(data []byte) error {
parsed, err := ParseAnyInt64(string(data))
func (id *AnyInt64[P]) UnmarshalText(data []byte) error {
parsed, err := ParseAnyInt64[P](string(data))
if err != nil {
return err
}
*id = parsed
return nil
}

func (id AnyInt64) Value() (driver.Value, error) {
func (id AnyInt64[P]) Value() (driver.Value, error) {
if id.val <= 0 {
return nil, ErrNonPositiveInt
}
return id.val, nil
}

func (id *AnyInt64) Scan(src any) error {
func (id *AnyInt64[P]) Scan(src any) error {
var v int64
switch sv := src.(type) {
case int64:
Expand Down
65 changes: 37 additions & 28 deletions anyuuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,84 +12,93 @@ import (

// AnyUUID is a UUIDv7 identifier with a runtime-configurable prefix.
// Unlike [UUID], the prefix is not fixed at compile time.
type AnyUUID struct {
//
// Use [AnyPrefix] as P for unconstrained prefixes, or a custom enum type
// implementing [VariablePrefixer] for a known set of variants.
type AnyUUID[P Prefixer] struct {
prefix P
val uuid.UUID
prefix string
}

func NewAnyUUID(prefix string) (AnyUUID, error) {
func NewAnyUUID[P Prefixer](p P) (AnyUUID[P], error) {
u, err := uuid.NewV7()
if err != nil {
return AnyUUID{}, err
return AnyUUID[P]{}, err
}
return AnyUUID{val: u, prefix: prefix}, nil
return AnyUUID[P]{val: u, prefix: p}, nil
}

func AnyUUIDFrom(prefix string, u uuid.UUID) (AnyUUID, error) {
func AnyUUIDFrom[P Prefixer](p P, u uuid.UUID) (AnyUUID[P], error) {
if u.Version() != 7 {
return AnyUUID{}, ErrOnlyV7
return AnyUUID[P]{}, ErrOnlyV7
}
return AnyUUID{val: u, prefix: prefix}, nil
return AnyUUID[P]{val: u, prefix: p}, nil
}

func ParseAnyUUID(s string) (AnyUUID, error) {
func ParseAnyUUID[P Prefixer](s string) (AnyUUID[P], error) {
var p P
j := strings.LastIndex(s, "_") + 1
pref, suffix := s[:max(0, j-1)], s[j:]
if len(suffix) != uuidSuffixLen {
return AnyUUID{}, fmt.Errorf("typeid: invalid format: %q", s)
return AnyUUID[P]{}, fmt.Errorf("typeid: invalid format: %q", s)
}
if vp, ok := any(&p).(VariablePrefixer); ok {
vp.ParsePrefix(pref)
}
if p.Prefix() != pref {
return AnyUUID[P]{}, fmt.Errorf("typeid: invalid prefix: %q", pref)
}
b, err := decodeBase32UUID(suffix)
if err != nil {
return AnyUUID{}, err
return AnyUUID[P]{}, err
}
u := uuid.UUID(b)
if u.Version() != 7 {
return AnyUUID{}, ErrOnlyV7
return AnyUUID[P]{}, ErrOnlyV7
}
return AnyUUID{val: u, prefix: pref}, nil
return AnyUUID[P]{val: u, prefix: p}, nil
}

func (id AnyUUID) UUID() uuid.UUID { return id.val }
func (id AnyUUID) Prefix() string { return id.prefix }
func (id *AnyUUID) SetPrefix(s string) {
id.prefix = s
}
func (id AnyUUID[P]) UUID() uuid.UUID { return id.val }
func (id AnyUUID[P]) Prefix() string { return id.prefix.Prefix() }
func (id AnyUUID[P]) Variant() P { return id.prefix }
func (id *AnyUUID[P]) SetPrefix(p P) { id.prefix = p }

func (id AnyUUID) appendText(dst []byte) []byte {
return appendBase32UUID(dst, id.prefix, id.val)
func (id AnyUUID[P]) appendText(dst []byte) []byte {
return appendBase32UUID(dst, id.prefix.Prefix(), id.val)
}

func (id AnyUUID) String() string {
func (id AnyUUID[P]) String() string {
var buf [64]byte
return string(id.appendText(buf[:0]))
}

func (id AnyUUID) IsZero() bool { return id.val == uuid.UUID{} }
func (id AnyUUID[P]) IsZero() bool { return id.val == uuid.UUID{} }

func (id AnyUUID) MarshalText() ([]byte, error) {
func (id AnyUUID[P]) MarshalText() ([]byte, error) {
if id.IsZero() {
return nil, ErrZeroUUID
}
return id.appendText(nil), nil
}

func (id *AnyUUID) UnmarshalText(data []byte) error {
parsed, err := ParseAnyUUID(string(data))
func (id *AnyUUID[P]) UnmarshalText(data []byte) error {
parsed, err := ParseAnyUUID[P](string(data))
if err != nil {
return err
}
*id = parsed
return nil
}

func (id AnyUUID) Value() (driver.Value, error) {
func (id AnyUUID[P]) Value() (driver.Value, error) {
if id.IsZero() {
return nil, ErrZeroUUID
}
return id.val.String(), nil
}

func (id *AnyUUID) Scan(src any) (err error) {
func (id *AnyUUID[P]) Scan(src any) (err error) {
var u uuid.UUID
switch v := src.(type) {
case string:
Expand Down Expand Up @@ -118,7 +127,7 @@ func (id *AnyUUID) Scan(src any) (err error) {
}

// GetTime extracts the millisecond-precision creation timestamp from the UUIDv7.
func (id AnyUUID) GetTime() time.Time {
func (id AnyUUID[P]) GetTime() time.Time {
ms := int64(binary.BigEndian.Uint16(id.val[:2]))<<32 | int64(binary.BigEndian.Uint32(id.val[2:6]))
return time.UnixMilli(ms)
}
66 changes: 62 additions & 4 deletions int64_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func BenchmarkInt64_Parse(b *testing.B) {
func ExampleAnyInt64_switchToTypedInt64() {
const payload = `{"id":"org_01hf7yat00c1s"}`
type Request struct {
ID typeid.AnyInt64 `json:"id"`
ID typeid.AnyInt64[typeid.AnyPrefix] `json:"id"`
}
var req Request
if err := json.Unmarshal([]byte(payload), &req); err != nil {
Expand Down Expand Up @@ -270,7 +270,7 @@ func ExampleAnyInt64_switchToTypedInt64() {

func TestAnyInt64_json(t *testing.T) {
type Request struct {
ID typeid.AnyInt64 `json:"id"`
ID typeid.AnyInt64[typeid.AnyPrefix] `json:"id"`
}

suffix := "01hf7yat00c1s"
Expand All @@ -292,7 +292,7 @@ func TestAnyInt64_json(t *testing.T) {

func TestAnyInt64_prefixAndSetPrefix(t *testing.T) {
suffix := "01hf7yat00c1s"
id, err := typeid.ParseAnyInt64("foo_" + suffix)
id, err := typeid.ParseAnyInt64[typeid.AnyPrefix]("foo_" + suffix)
if err != nil {
t.Fatal(err)
}
Expand All @@ -312,7 +312,7 @@ func TestAnyInt64_prefixAndSetPrefix(t *testing.T) {

func TestAnyInt64_narrowToOrgPrefix(t *testing.T) {
suffix := "01hf7yat00c1s"
anyID, err := typeid.ParseAnyInt64("org_" + suffix)
anyID, err := typeid.ParseAnyInt64[typeid.AnyPrefix]("org_" + suffix)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -352,3 +352,61 @@ func TestInt64_Sortable(t *testing.T) {
t.Errorf("expected a < b lexicographically\n a = %s\n b = %s", a, b)
}
}

// -- Variable prefix (apiKeyMode) tests for Int64 --

type ApiKeyInt64ID = typeid.AnyInt64[apiKeyMode]

func TestAnyInt64_VariablePrefix_Parse(t *testing.T) {
id, _ := typeid.NewAnyInt64(apiKeyLive)
s := id.String()
parsed, err := typeid.ParseAnyInt64[apiKeyMode](s)
if err != nil {
t.Fatalf("ParseAnyInt64: %v", err)
}
if parsed.Variant() != apiKeyLive {
t.Errorf("Variant() = %d, want %d", parsed.Variant(), apiKeyLive)
}
if parsed.Prefix() != "api_key" {
t.Errorf("Prefix() = %q, want %q", parsed.Prefix(), "api_key")
}
}

func TestAnyInt64_VariablePrefix_RejectsUnknown(t *testing.T) {
id, _ := typeid.NewAnyInt64(apiKeyLive)
// Swap prefix to something unknown
s := strings.Replace(id.String(), "api_key_", "bogus_", 1)
_, err := typeid.ParseAnyInt64[apiKeyMode](s)
if err == nil {
t.Fatal("expected error for unknown prefix")
}
}

func TestAnyInt64_VariablePrefix_Roundtrip(t *testing.T) {
id, _ := typeid.NewAnyInt64(apiKeySandbox)
s := id.String()
parsed, err := typeid.ParseAnyInt64[apiKeyMode](s)
if err != nil {
t.Fatalf("roundtrip: %v", err)
}
if parsed.Int64() != id.Int64() {
t.Error("Int64 mismatch")
}
if parsed.Variant() != apiKeySandbox {
t.Errorf("Variant() = %d, want %d", parsed.Variant(), apiKeySandbox)
}
if parsed.String() != s {
t.Errorf("String() = %q, want %q", parsed.String(), s)
}
}

func TestAnyInt64_VariablePrefix_SetPrefix(t *testing.T) {
id, _ := typeid.NewAnyInt64(apiKeyLive)
id.SetPrefix(apiKeySandbox)
if id.Variant() != apiKeySandbox {
t.Errorf("Variant() = %d, want %d", id.Variant(), apiKeySandbox)
}
if id.Prefix() != "api_key_sandbox" {
t.Errorf("Prefix() = %q, want %q", id.Prefix(), "api_key_sandbox")
}
}
17 changes: 17 additions & 0 deletions typeid.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ type Prefixer interface {
Prefix() string
}

// VariablePrefixer is optionally implemented by prefix types that accept
// multiple string representations (e.g. "api_key" and "api_key_sandbox").
// ParsePrefix sets the receiver to the variant matching s and reports success.
type VariablePrefixer interface {
ParsePrefix(s string) bool
}

// AnyPrefix accepts any prefix string. Use it as the type parameter for
// [AnyUUID] or [AnyInt64] when the set of valid prefixes is not known at
// compile time:
//
// type FlexID = typeid.AnyUUID[typeid.AnyPrefix]
type AnyPrefix string

func (p AnyPrefix) Prefix() string { return string(p) }
func (p *AnyPrefix) ParsePrefix(s string) bool { *p = AnyPrefix(s); return true }

var (
ErrOnlyV7 = errors.New("typeid: only UUIDv7 is supported")
ErrZeroUUID = errors.New("typeid: zero UUID")
Expand Down
Loading