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

// AnyInt64 is a compact identifier with a runtime-configurable prefix.
// Unlike [Int64], the prefix is not fixed at compile time.
//
// 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
type AnyInt64 struct {
val int64
prefix string
}

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

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

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

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

func ParseAnyInt64[P Prefixer](s string) (AnyInt64[P], error) {
var p P
func ParseAnyInt64(s string) (AnyInt64, error) {
j := strings.LastIndex(s, "_") + 1
pref, suffix := s[:max(0, j-1)], s[j:]
if len(suffix) != int64SuffixLen {
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)
return AnyInt64{}, fmt.Errorf("typeid: invalid format: %q", s)
}
v, err := decodeBase32Int64(suffix)
if err != nil {
return AnyInt64[P]{}, err
return AnyInt64{}, err
}
if v <= 0 {
return AnyInt64[P]{}, ErrNonPositiveInt
return AnyInt64{}, ErrNonPositiveInt
}
return AnyInt64[P]{val: v, prefix: p}, nil
return AnyInt64{val: v, prefix: pref}, nil
}

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) 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]) appendText(dst []byte) []byte {
return appendBase32Int64(dst, id.prefix.Prefix(), id.val)
func (id AnyInt64) appendText(dst []byte) []byte {
return appendBase32Int64(dst, id.prefix, id.val)
}

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

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

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

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

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

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

// AnyUUID is a UUIDv7 identifier with a runtime-configurable prefix.
// Unlike [UUID], the prefix is not fixed at compile time.
//
// 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
type AnyUUID struct {
val uuid.UUID
prefix string
}

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

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

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

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) 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]) appendText(dst []byte) []byte {
return appendBase32UUID(dst, id.prefix.Prefix(), id.val)
func (id AnyUUID) appendText(dst []byte) []byte {
return appendBase32UUID(dst, id.prefix, id.val)
}

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

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

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

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

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

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

// GetTime extracts the millisecond-precision creation timestamp from the UUIDv7.
func (id AnyUUID[P]) GetTime() time.Time {
func (id AnyUUID) 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: 4 additions & 62 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[typeid.AnyPrefix] `json:"id"`
ID typeid.AnyInt64 `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[typeid.AnyPrefix] `json:"id"`
ID typeid.AnyInt64 `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[typeid.AnyPrefix]("foo_" + suffix)
id, err := typeid.ParseAnyInt64("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[typeid.AnyPrefix]("org_" + suffix)
anyID, err := typeid.ParseAnyInt64("org_" + suffix)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -352,61 +352,3 @@ 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: 0 additions & 17 deletions typeid.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,6 @@ 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