// Package ident — единственная точка генерации и разбора идентификаторов // домена. Идентификатор — ULID (26 символов Crockford base32), канонический // вид — lowercase; на входных границах (URL, формы) значение прогоняется // через Parse до обращения к хранилищу, потому что сравнение строк в SQLite // побайтовое. package ident import ( "crypto/rand" "fmt" "strings" "sync" "time" "github.com/oklog/ulid/v2" ) // entropy — monotonic-источник: при равной миллисекунде timestamp'а энтропия // инкрементируется, так что id, выданные подряд, сохраняют порядок выдачи. var ( mu sync.Mutex entropy = ulid.Monotonic(rand.Reader, 0) ) // NewID возвращает новый идентификатор (текущее время). func NewID() string { return NewIDAt(time.Now()) } // NewIDAt возвращает идентификатор с timestamp-частью из t. Используется // миграциями для бэкфилла: сортировка id сохраняет историческую хронологию // (created_at имеет секундное разрешение — равные метки упорядочивает // monotonic-энтропия в порядке вызовов). func NewIDAt(t time.Time) string { mu.Lock() defer mu.Unlock() id := ulid.MustNew(ulid.Timestamp(t.UTC()), entropy) return strings.ToLower(id.String()) } // Parse валидирует внешний идентификатор и нормализует его к каноническому // lowercase-виду. Регистр входа не важен (base32 ULID case-insensitive). func Parse(s string) (string, error) { s = strings.TrimSpace(s) u, err := ulid.ParseStrict(strings.ToUpper(s)) if err != nil { return "", fmt.Errorf("ident: parse %q: %w", s, err) } return strings.ToLower(u.String()), nil }