import type { AppDomain, HostKind, PathKind } from './api'; /** Guess a path kind from the literal user input. The dashboard pre-fills * the kind selector but the user can override (the backend trusts the * selection — selecting `exact` for `/greet/:name` matches it literally). */ export function guessPathKind(raw: string): PathKind { if (raw.endsWith('/*')) return 'prefix'; if (/(^|\/):[a-zA-Z_]/.test(raw)) return 'param'; return 'exact'; } /** Guess a host kind. Empty → any, leading "*." → wildcard, else strict. */ export function guessHostKind(raw: string): HostKind { if (!raw.trim()) return 'any'; if (raw.startsWith('*.')) return 'wildcard'; return 'strict'; } /** Warn-not-block: does the user's selection match what we'd guess * from the input? Used by the routing form for the soft mismatch * hint. Returns null when no warning is needed. */ export function pathKindMismatchWarning(raw: string, kind: PathKind): string | null { const guessed = guessPathKind(raw); if (guessed === kind) return null; const hint = guessed === 'param' ? 'this looks like a param pattern (contains `:name`)' : guessed === 'prefix' ? 'this looks like a prefix pattern (ends with `/*`)' : 'this looks like a literal path'; return `Selected kind is "${kind}", but ${hint}. Routing will use the selected kind.`; } /** Parse the user's free-text host input into the (kind, stored host) * pair the API expects. Mirrors the dashboard's pre-existing storage * convention: wildcard route hosts are stored as the bare suffix * (`*.foo.com` → `foo.com`); display-time formatting re-adds the `*.`. */ export interface ParsedHostInput { kind: HostKind; /** What gets sent in the API payload as `host`. */ host: string; /** Canonical display form, for chips/labels. */ display: string; } export function parseHostInput(raw: string): ParsedHostInput { const trimmed = raw.trim(); if (trimmed === '' || trimmed === '*') { return { kind: 'any', host: '', display: '*' }; } if (trimmed.startsWith('*.')) { const suffix = trimmed.slice(2); return { kind: 'wildcard', host: suffix, display: `*.${suffix}` }; } return { kind: 'strict', host: trimmed, display: trimmed }; } /** Frontend mirror of `validate_route_host_against_app` in * crates/manager-core/src/route_admin.rs. Lets us surface a live * warning instead of waiting for a 422. The server runs the same * check on submit — this is purely an early signal. */ export type HostClaimCheck = | { ok: true; matched: string } | { ok: false; reason: string }; export function checkHostAgainstClaims( parsed: ParsedHostInput, claims: AppDomain[] ): HostClaimCheck { if (parsed.kind === 'any') return { ok: true, matched: '*' }; if (claims.length === 0) { return { ok: false, reason: 'this app has no domain claims yet — add one in the app’s Domains tab' }; } const hostLower = parsed.host.toLowerCase(); for (const claim of claims) { const claimLower = claim.pattern.toLowerCase(); const claimSuffix = claimLower.split('.').slice(1).join('.'); if (parsed.kind === 'strict') { if (claim.shape === 'exact' && hostLower === claimLower) { return { ok: true, matched: claim.pattern }; } if ( (claim.shape === 'wildcard' || claim.shape === 'parameterized') && claimSuffix && hostLower.endsWith(`.${claimSuffix}`) ) { return { ok: true, matched: claim.pattern }; } } else { // wildcard route if ( (claim.shape === 'wildcard' || claim.shape === 'parameterized') && claimSuffix === hostLower ) { return { ok: true, matched: claim.pattern }; } } } return { ok: false, reason: `${parsed.display} is not covered by any of this app’s claims` }; } /** Suggestion strings for the host input's . We always * include the wildcard `*` (any host) as the first option, then the * app's claims rendered in the form the route input expects. */ export function hostSuggestions(claims: AppDomain[]): string[] { const items: string[] = ['*']; for (const claim of claims) { if (claim.shape === 'exact') { items.push(claim.pattern); } else { // Both wildcard and parameterized claims are usable as a // wildcard route. Render as `*.suffix` — we can't preserve // the {param} binding name through the current route form. const suffix = claim.pattern.split('.').slice(1).join('.'); if (suffix) items.push(`*.${suffix}`); } } // Dedupe while preserving order. return [...new Set(items)]; }