"""Slug helpers for posts (and, eventually, any other slug-keyed row). A slug is the URL-safe identifier used in public post URLs. Keeping the algorithm tiny, dependency-free, and in its own module makes it easy to test in isolation and to reuse for the Phase 4 admin create/update flow. Rules applied by :func:`slugify`: - lowercase the input - replace every run of non-alphanumeric characters with a single ``-`` - collapse consecutive ``-`` runs - strip leading and trailing ``-`` - never return an empty string — callers that pass empty / all-punctuation input get a deterministic fallback (``"post"``) so they can still build a valid URL. :func:`ensure_unique` suffixes ``-2``, ``-3`` ... on collision, checking the database row presence via a callable the caller supplies. Keeping the DB access injectable keeps this module trivially testable. """ from __future__ import annotations import re from typing import Callable # Single-pass regex collapses any run of non-alphanumeric characters # into a single hyphen. Unicode letters are NOT preserved — the URL # column is ASCII-safe by design, so exotic characters collapse away. _NON_ALNUM_RE: re.Pattern[str] = re.compile(r"[^a-z0-9]+") # Fallback slug when the user submits a title that slugifies to the # empty string (e.g. only punctuation). Keeps write paths from crashing # on pathological input while remaining human-readable in the URL. _FALLBACK_SLUG: str = "post" def slugify(title: str) -> str: """Return a URL-safe slug derived from ``title``. Parameters ---------- title: Human-authored title, typically from an admin form. Treated as untrusted — no assumption about length or character set. Returns ------- str A lowercased, hyphen-separated string containing only ``[a-z0-9-]`` with no leading or trailing hyphens. Never empty; returns :data:`_FALLBACK_SLUG` if the input produced an empty result after normalization. """ lowered = (title or "").lower() collapsed = _NON_ALNUM_RE.sub("-", lowered).strip("-") if not collapsed: return _FALLBACK_SLUG return collapsed def ensure_unique( base: str, exists: Callable[[str], bool], *, max_attempts: int = 1000, ) -> str: """Return a slug not currently in use, suffixing ``-2`` / ``-3`` as needed. Parameters ---------- base: Starting slug — typically the output of :func:`slugify`. exists: Callable that returns ``True`` if the candidate slug is already taken. The admin service passes a closure that hits the DB. max_attempts: Defensive bound on suffix-iteration so a degenerate ``exists`` callable can never spin forever. 1000 is wildly more than any realistic collision rate. Returns ------- str A slug ``exists`` returned ``False`` for. Raises :class:`RuntimeError` in the pathological case where every suffix is taken up to ``max_attempts``. """ if not exists(base): return base # Start at -2 because the bare slug is already taken. -1 would be # reserved for the same row we're competing with, which is confusing # in the DB. for n in range(2, max_attempts + 1): candidate = f"{base}-{n}" if not exists(candidate): return candidate raise RuntimeError( f"could not allocate a unique slug after {max_attempts} attempts" f" (base={base!r})" )