defmodule Planck.Headless.SessionName do alias Planck.Agent.Session @moduledoc """ Generates and sanitizes human-readable session names in `-` format. Names are embedded in SQLite filenames alongside the session id: `/_.db`. Both components are URL-safe and contain only `[a-z0-9-]` characters, with `_` reserved as the separator. ## Generation `generate/1` picks a random adjective + noun pair and retries (up to a configurable limit) if the resulting name already exists on disk. ## Sanitization `sanitize/1` normalises a user-provided string to the same character class: lowercase, spaces and underscores become hyphens, and any other character outside `[a-z0-9-]` is stripped. The result is truncated to 64 characters. If sanitization produces an empty string, `{:error, :invalid}` is returned. """ @adjectives ~w( amber ancient angular arctic bold brave bright calm careful classic clever cold cool cosmic crisp curious damp dark dawn deep deft dense dim distant dry dusty dynamic eager easy electric elegant epic exact faint fast fierce fine fresh frozen gentle glacial grand heavy hidden hollow humble icy idle inner jolly keen kind late lazy light liquid lost loud lucky lunar lush mellow mild misty modern narrow neat noble north novel odd old open pale patient plain polar prime proud quick quiet rapid rare ready rigid rough round royal rustic sharp sleek slim slow small smooth soft solar solid steady still stone strong swift tall tidy tiny tough vivid warm wide wild winding wise wooden young ) @nouns ~w( apple apricot avocado banana berry blossom blueberry broccoli cantaloupe carrot celery cherry chestnut citrus clementine coconut cucumber currant dates durian elderberry fennel fig garlic ginger grape grapefruit guava honeydew huckleberry jackfruit kiwi kumquat leek lemon lettuce lime lychee mango mangosteen melon mulberry nectarine olive onion orange papaya parsley peach pear pepper persimmon pineapple plum pomelo potato pumpkin quince radish raspberry rhubarb spinach starfruit strawberry tamarind tangerine tomato turnip walnut watermelon ) @max_retries 20 @doc """ Generate a unique `-` name not already present in `sessions_dir`. Retries up to #{@max_retries} times on collision. Returns `{:ok, name}` or `{:error, :exhausted}` if all retries collide. """ @spec generate(Path.t()) :: {:ok, String.t()} | {:error, :exhausted} def generate(sessions_dir, retries \\ @max_retries) def generate(_sessions_dir, 0), do: {:error, :exhausted} def generate(sessions_dir, retries) do name = random_name() case Session.find_by_name(sessions_dir, name) do {:error, :not_found} -> {:ok, name} {:ok, _, _} -> generate(sessions_dir, retries - 1) end end @doc """ Sanitize a user-provided string to a valid session name. - Lowercased. - Spaces and underscores converted to hyphens. - Characters outside `[a-z0-9-]` stripped. - Consecutive hyphens collapsed to one. - Leading and trailing hyphens removed. - Truncated to 64 characters. Returns `{:ok, name}` or `{:error, :invalid}` if the result is empty. """ @spec sanitize(String.t()) :: {:ok, String.t()} | {:error, :invalid} def sanitize(input) when is_binary(input) do result = input |> String.downcase() |> String.replace(~r/[\s_]+/, "-") |> String.replace(~r/[^a-z0-9-]/, "") |> String.replace(~r/-{2,}/, "-") |> String.trim("-") |> String.slice(0, 64) if result == "", do: {:error, :invalid}, else: {:ok, result} end # --------------------------------------------------------------------------- # Private # --------------------------------------------------------------------------- @spec random_name() :: String.t() defp random_name do adjective = Enum.random(@adjectives) noun = Enum.random(@nouns) "#{adjective}-#{noun}" end end