Documentation
¶
Overview ¶
Package slug derives URL-safe plan slugs from human titles. The derivation is the single source of truth shared by the v5 migration backfill in internal/db and the runtime CreatePlan path in internal/store — keeping the algorithm in one place means a v5 DB migrated from v4 and a v5 DB created fresh agree on the slug for any given title.
Algorithm (Derive):
- Lowercase the input.
- Map every Unicode codepoint that is not ASCII letter/digit to '-' (so em-dashes, fancy punctuation, and non-ASCII letters all become a separator). We do not transliterate — "café" becomes "caf" rather than "cafe". The goal is a deterministic ASCII slug, not faithful Unicode preservation.
- Collapse runs of '-' and trim leading/trailing '-'.
- If the result is empty (title was punctuation-only), fall back to the literal "plan".
- If the result is all digits (e.g. "42"), append "-plan" so the slug never looks like a numeric id at a routing layer that might parse it as one.
- Cap the result at slugMaxLen runes. If truncation lands inside a word, prefer to cut at the last '-' within the cap so the slug stays composed of whole pieces. Strip any trailing '-' left over.
DeriveUnique wraps Derive with a deterministic "-N" dedup loop, where the existence check is injected so the same helper drives both the migration backfill (SELECT against the in-flight tx) and runtime inserts (SELECT against the live DB).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrTooManyCollisions = fmt.Errorf("slug: more than %d collisions", maxCollisions)
ErrTooManyCollisions surfaces when DeriveUnique exhausts the dedup budget. Callers should wrap it with store.ErrInvalid (or its own equivalent) so the CLI envelope reports code:"invalid".
Functions ¶
func Derive ¶
Derive is the pure title→slug transform. Deterministic, no I/O. Callers that need uniqueness across a table use DeriveUnique.
func DeriveUnique ¶
func DeriveUnique(title string, exists ExistsFunc) (string, error)
DeriveUnique computes the base slug from title, then walks the dedup sequence base, base-2, base-3, …, base-maxCollisions, asking exists at each step. Returns the first slug for which exists is false. Wraps ErrTooManyCollisions when every candidate is taken so the CLI/migration can surface a code:"invalid" envelope rather than silently corrupting the unique index.
Length contract: the dedup suffix can extend the slug beyond slugMaxLen by up to len("-100") = 4 bytes. We choose to honor uniqueness over the cap because the cap is cosmetic and uniqueness is correctness — the unique index would reject anything else.
Types ¶
type ExistsFunc ¶
ExistsFunc returns (true, nil) when slug is already taken, (false, nil) when free, or (_, err) on lookup failure. Injected so the same dedup loop drives the migration tx and the runtime store.