ids

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 10 Imported by: 0

README

ids

gokit/ids — kit-standard prefixed-ULID utility. API-сервисы на gokit'е tag'ируют идентификаторы коротким type-префиксом (user_01H…, acc_01H…, prod_01H…). Этот pattern был везде, но unstandardised — каждый consumer переписывал NewID(prefix) / ParseID(prefix, s) / FormatID(prefix, raw) поверх oklog/ulid/v2. С v1.1.0 это в kit'е.

Quickstart

import "github.com/theizzatbek/gokit/ids"

id := ids.New("prod_")           // "prod_01H0000000000000000000000000"

raw, err := ids.Parse("prod_", id)  // [16]byte, ready to INSERT into uuid column
// err = *errs.Error{Code: ids.CodeBadPrefix | ids.CodeBadSuffix}

reconstructed := ids.Format("prod_", raw)  // same string as `id`

raw — это ровно 16 байт, которые pgx пишет в Postgres uuid column через [16]byte codec, который kit регистрирует в db.Connect (см. db/uuid_codec.go, шиппит в v1.0.1). То есть Parserepo.Insertrepo.GetFormat round-trip через uuid column работает без явной обёртки pgtype.UUID{}.

Wire shape

Каждый ID — это <prefix><26-char Crockford-Base32 ULID>. Suffix decodit'ся ровно в 16 raw bytes:

6 bytes ms-precision Unix timestamp || 10 bytes randomness
  • Time-sortable: lexicographic order строк совпадает с creation order at ms resolution.
  • Monotonic within process: два New() вызова в одной миллисекунде дают strictly increasing IDs (per ULID monotonic-entropy contract).
  • Goroutine-safe: New() сериализован вокруг package-level mutex'а так что concurrent calls не race'ят entropy source.

Validator tag

Для declarative-валидации входящих ID в DTO:

v := validator.New(validator.WithRequiredStructEnabled())
ids.RegisterValidator(v)
svc, _ := service.New[AppCtx, Claims](ctx, cfg, service.WithValidator(v))

type CreatePolicyReq struct {
    ProductID string `json:"product_id" validate:"required,id_prefix=prod_"`
}

Field, failing tag → normal validator.ValidationErrors entry → fibermap.ErrsvalBindError picks it up как 400 с per-field details[] без дополнительной обвязки.

Programmatic tag assembly (для code-generated DTO):

type Field struct {
    ID string `validate:"required,..."`  // assembled at gen time
}

tag := "required," + ids.Tag("prod_")    // ⇒ "required,id_prefix=prod_"

Error codes

Все Parse-ошибки — *errs.Error{Kind: Validation, Code: ...}:

Code Когда
CodeBadPrefix ("id_bad_prefix") Input не начинается с expected prefix'а (включая length mismatch / empty input).
CodeBadSuffix ("id_bad_suffix") 26-char tail не валидный Crockford-Base32 ULID (wrong length, illegal character, ULID-overflow).

Wire-safe для surface'а в HTTP 400 responses.

Sentinel errors (для errors.Is):

Sentinel Wrapping behavior
ErrBadPrefix Wrapped by the *errs.Error returned from Parse when prefix mismatches.
ErrBadSuffix Wrapped when suffix isn't a valid ULID.

Most callers should match on e.Code — codes — semver-stable; sentinel identity — нет.

Конвенции prefix'а

Kit ничего не enforce'ит по содержанию prefix'а — это plain string concatenation:

  • Краткие, lowercase, отделены _ — традиция Stripe (sk_test_, cus_), GitHub (ghp_), и т.д.
  • Один префикс на entity-type: user_, acc_, prod_, pol_, lic_.
  • Длина 3-5 chars обычно достаточно — длиннее → IDs становятся unwieldy в URL и логах.

Per-service mapping prefix → entity-type живёт в каком-нибудь internal/domain/ids.go каждого сервиса:

package domain

import "github.com/theizzatbek/gokit/ids"

func NewUserID() string    { return ids.New("user_") }
func NewProductID() string { return ids.New("prod_") }
func NewPolicyID() string  { return ids.New("pol_") }

func ParseUserID(s string) ([16]byte, error)    { return ids.Parse("user_", s) }
func ParseProductID(s string) ([16]byte, error) { return ids.Parse("prod_", s) }

Этот файл — единственное место в сервисе где префиксы упоминаются как литералы.

Не для всех ID-кейсов

ids/ для public-surfaced application-уровневых identifier'ов. НЕ для:

  • Database-row internal IDs — для uuid PRIMARY KEY DEFAULT gen_random_uuid() ничего prefix'ovaть не надо; raw UUID этот case покрывает естественно.
  • Cryptographic tokens — refresh / access tokens / API keys имеют свои конструкторы (Argon2 / Ed25519 / HMAC), ids непригоден.
  • Request IDs — для request-ID conventions используйте fibermap.LocalsRequestID (стандартный X-Request-ID flow).
  • High-entropy nonces — для one-time tokens используйте crypto/rand.Read напрямую; ULID monotonic ordering это leak'ает.

Documentation

Overview

Package ids is the kit's standard prefixed-ULID utility.

Services on gokit conventionally tag identifiers with a short type-prefix: `user_01H…`, `acc_01H…`, `prod_01H…`. The pattern is everywhere but was unstandardised — every consumer rewrote the same `NewID(prefix)` / `ParseID(prefix, s)` / `FormatID(prefix, raw)` helpers on top of github.com/oklog/ulid/v2. This package is the single canonical implementation.

Wire shape

Every ID is `<prefix><26-char Crockford-Base32 ULID>`. Crockford Base32 is the ULID-spec encoding — case-insensitive on Parse, but New / Format always emit uppercase for stability.

The 26-char suffix decodes to exactly 16 raw bytes (the binary ULID layout: 6 bytes ms-precision timestamp + 10 bytes randomness). Same 16 bytes pgx writes to a Postgres `uuid` column via the [16]byte codec the kit registers in db.Connect (see `db/uuid_codec.go`, shipped in v1.0.1).

Functions

  • New — mint a new prefixed ID. Time-sortable. Monotonic within a process under contention.
  • Parse — validate + strip prefix, return raw 16 bytes for storage as `uuid`.
  • Format — inverse of Parse for callers holding raw bytes (typical: row scanned from a `uuid` column).
  • RegisterValidator — wire a `validate:"prefix=prod_"` struct tag for declarative DTO validation.

Error codes

All parse-stage errors are *errs.Error of Kind = Validation:

  • CodeBadPrefix — input doesn't start with the expected prefix (including length mismatch or empty input).
  • CodeBadSuffix — 26-char suffix isn't a valid Crockford-Base32 ULID (wrong length, illegal character, etc).

`errors.Is` works against the package-level sentinels ErrBadPrefix and ErrBadSuffix for branching code, but most callers should match on `e.Code` for stable wire / log behaviour.

Goroutine safety

New is goroutine-safe: it serialises around a package-level monotonic entropy source so two concurrent calls in the same millisecond still produce strictly increasing ULIDs (per the ULID spec). Parse / Format are pure functions over their inputs.

Example

Example mints a fresh prefixed ULID and shows the round-trip: New → Parse → Format returns the original string. New is time-based and random, so the value differs every run — the invariants below hold regardless.

package main

import (
	"fmt"
	"strings"

	"github.com/theizzatbek/gokit/ids"
)

func main() {
	id := ids.New("user_")

	raw, err := ids.Parse("user_", id)

	fmt.Println(strings.HasPrefix(id, "user_"))
	fmt.Println(err == nil)
	fmt.Println(ids.Format("user_", raw) == id)
}
Output:
true
true
true

Index

Examples

Constants

View Source
const (
	// CodeBadPrefix — input doesn't start with the prefix the caller
	// supplied. Also returned when the input is shorter than the
	// prefix or empty. Wire-safe to surface in HTTP 400 responses.
	CodeBadPrefix = "id_bad_prefix"

	// CodeBadSuffix — the 26-char tail after the prefix isn't a
	// valid Crockford-Base32 ULID. Covers wrong length, illegal
	// character, ULID-overflow. Wire-safe to surface in HTTP 400.
	CodeBadSuffix = "id_bad_suffix"
)

Stable error codes returned in *errs.Error.Code from the ids package. Downstream alerting / log-routing can match on these constants stably.

View Source
const ValidatorTag = "id_prefix"

ValidatorTag is the struct-tag name RegisterValidator wires. Stable across the kit's semver contract; documented here so callers can grep for it.

Variables

View Source
var (
	// ErrBadPrefix is wrapped by the *errs.Error returned from Parse
	// when the input doesn't match the supplied prefix.
	ErrBadPrefix = errors.New("ids: bad prefix")

	// ErrBadSuffix is wrapped by the *errs.Error returned from Parse
	// when the suffix isn't a valid ULID.
	ErrBadSuffix = errors.New("ids: bad suffix")
)

Sentinel errors for errors.Is checks. Most callers should branch on *errs.Error.Code instead — the codes are stable across the kit's semver contract; sentinel identity is not.

Functions

func Format

func Format(prefix string, raw [16]byte) string

Format is the inverse of Parse for callers holding raw 16-byte ID material (typically a row scanned from a pgx `uuid` column). The output is the canonical wire form: `prefix + 26-char uppercase Crockford-Base32`.

Format does not validate raw — any 16 bytes encode cleanly to a 26-char ULID string. Callers that scanned from an authoritative source (DB, Parse output) can trust the result.

func New

func New(prefix string) string

New returns "<prefix><26-char Crockford-Base32 ULID>".

The 16-byte binary layout is the ULID spec's:

6 bytes ms-precision Unix timestamp || 10 bytes randomness

IDs are time-sortable (lexicographic order matches creation order at ms resolution) and monotonic within a single process — two New() calls in the same millisecond produce IDs that compare as strictly increasing per the ULID monotonic-entropy contract.

New is safe for concurrent use; the entropy source is guarded by a package-level lock so two goroutines colliding in the same millisecond see deterministic monotonic increments rather than the underlying ulid package's panic-on-overflow behaviour.

func Parse

func Parse(prefix, s string) ([16]byte, error)

Parse validates that s starts with prefix and that the 26-char tail decodes as a Crockford-Base32 ULID, returning the raw 16 bytes for storage as a Postgres uuid column (or any other 16-byte-keyed store).

Error mapping:

*errs.Error{Kind: Validation, Code: CodeBadPrefix}
    when s doesn't start with prefix (or is shorter than prefix).

*errs.Error{Kind: Validation, Code: CodeBadSuffix}
    when the tail isn't a valid ULID (wrong length, illegal
    character, overflow).

Callers that want errors.Is can match against ErrBadPrefix / ErrBadSuffix; most code should match on e.Code instead — the codes are part of the kit's semver-stable contract.

Example

ExampleParse validates a known ID and strips the prefix, returning the raw 16 bytes you'd store in a Postgres uuid column. Format is the inverse and always re-emits uppercase Crockford Base32.

package main

import (
	"fmt"

	"github.com/theizzatbek/gokit/ids"
)

func main() {
	raw, err := ids.Parse("user_", "user_01ARZ3NDEKTSV4RRFFQ69G5FAV")
	if err != nil {
		panic(err)
	}
	fmt.Println(ids.Format("user_", raw))
}
Output:
user_01ARZ3NDEKTSV4RRFFQ69G5FAV
Example (BadPrefix)

ExampleParse_badPrefix shows the error contract: a prefix mismatch is a typed *errs.Error of KindValidation with a stable Code, so HTTP handlers map it to 400 without special-casing.

package main

import (
	"errors"
	"fmt"

	"github.com/theizzatbek/gokit/errs"
	"github.com/theizzatbek/gokit/ids"
)

func main() {
	_, err := ids.Parse("user_", "acct_01ARZ3NDEKTSV4RRFFQ69G5FAV")

	var e *errs.Error
	if errors.As(err, &e) {
		fmt.Println(e.Kind, e.Code)
	}
}
Output:
validation id_bad_prefix

func RegisterValidator

func RegisterValidator(v *validator.Validate) error

RegisterValidator wires the `validate:"id_prefix=<prefix>"` tag on the supplied *validator.Validate so DTOs can validate inbound IDs declaratively:

type CreatePolicyReq struct {
    ProductID string `json:"product_id" validate:"required,id_prefix=prod_"`
}

The tag enforces both halves of Parse: the field's string starts with the param-supplied prefix AND the tail is a valid 26-char Crockford-Base32 ULID. A field that fails either half surfaces a normal validator.ValidationErrors entry — the fibermap.ErrsvalBindError handler picks it up as a 400 with per-field Details[] without any additional wiring.

Returns the error from validator.RegisterValidation (rare: only on a duplicate registration on the same *validator.Validate instance, which is itself a programmer-error condition).

Pure-stdlib safety: the registered function does NOT depend on any package state beyond the `prefix` param, so it is safe to call against shared *validator.Validate instances (the typical case: [service.New] builds one default validator and the same instance binds every handler's bind step).

func Tag

func Tag(prefix string) string

Tag is a convenience for callers who want to assemble the tag string programmatically (typically in code-generated DTOs):

`validate:"required,` + ids.Tag("prod_") + `"`

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL