/** * The configuration pages' URL state, validated as pure functions so each * route's `validateSearch` stays a one-liner and every rejection is testable * without a router. * * A tab that is not one of the page's tabs falls back to that page's default: * a hand-typed `?tab=upstreams` on Protection opens Protection's first tab * rather than an empty panel. */ export const PROTECTION_TABS = ["groups", "sources"] as const; export type ProtectionTab = (typeof PROTECTION_TABS)[number]; export const RESOLUTION_TABS = ["upstreams", "records", "zones"] as const; export type ResolutionTab = (typeof RESOLUTION_TABS)[number]; /** * Both fields are optional so a link to the page need not spell out a tab it * does not care about — the same shape Overview's `period` uses. Validation * still resolves an unknown tab to the page default, so a *rendered* page * always has one. */ export interface ProtectionSearch { tab?: ProtectionTab; /** * The selected group. Absent until the page resolves one, which it then * writes back so a view of the page is a link. */ group?: number; } export interface ResolutionSearch { tab?: ResolutionTab; } function validateTab(options: readonly T[], value: unknown, fallback: T): T { return options.includes(value as T) ? (value as T) : fallback; } /** * A group row id. Ids are positive integers, so anything else — a fraction, a * string, a zero — is not an id that could ever exist and becomes `undefined`. * An id that is well-formed but unknown is a different case: the page has to * load the groups before it can tell, so it falls back there, not here. */ export function validateGroupId(value: unknown): number | undefined { return Number.isSafeInteger(value) && (value as number) > 0 ? (value as number) : undefined; } export function validateProtectionSearch(search: Record): ProtectionSearch { return { tab: validateTab(PROTECTION_TABS, search["tab"], "groups"), group: validateGroupId(search["group"]), }; } export function validateResolutionSearch(search: Record): ResolutionSearch { return { tab: validateTab(RESOLUTION_TABS, search["tab"], "upstreams") }; }