// Package ident — единая точка генерации и разбора идентификаторов (ULID). // // ULID сортируется по времени создания, компактен в логах и URL (без дефисов) // и глобально уникален across сущностей: поиск по голому id находит все // упоминания. Канонический вид — нижний регистр. package ident import ( "errors" "fmt" "strings" "time" "github.com/oklog/ulid/v2" ) // ErrInvalid — синтаксически некорректный идентификатор. Такой id трактуем // как несуществующую сущность, без похода в БД. var ErrInvalid = errors.New("некорректный идентификатор") // NewID возвращает новый ULID в нижнем регистре. func NewID() string { return strings.ToLower(ulid.Make().String()) } // TimeOf возвращает время создания идентификатора: UTC, секундная точность — // ровно та форма, в которой время хранится (см. store.Now). // // Нужен пересборке витрины: у тела, лежащего в архиве без учётной записи, // другого источника метки приёма нет. Дата каталога архива не годится — она // задаёт сутки, а порядок проигрывания нужен внутри суток. // // `.UTC()` здесь обязателен и не для красоты: ulid.Time собирает время через // time.Unix, то есть в локальной зоне машины, а Truncate работает с абсолютной // длительностью и зону не нормализует. Без приведения метка подобранного тела // сравнивалась бы с меткой из БД по-разному на разных машинах. func TimeOf(s string) (time.Time, error) { id, err := ulid.ParseStrict(strings.ToUpper(strings.TrimSpace(s))) if err != nil { return time.Time{}, fmt.Errorf("%w: %q", ErrInvalid, s) } return ulid.Time(id.Time()).UTC().Truncate(time.Second), nil } // Parse валидирует внешний идентификатор и приводит его к каноническому виду. // Вызывается на входных границах (HTTP, CLI) до запроса к БД: сравнение строк // в SQLite побайтовое, а base32 ULID при декодировании нечувствителен к // регистру. func Parse(s string) (string, error) { id, err := ulid.ParseStrict(strings.ToUpper(strings.TrimSpace(s))) if err != nil { return "", fmt.Errorf("%w: %q", ErrInvalid, s) } return strings.ToLower(id.String()), nil }