import { getRoute, ROUTE_RUNTIME_CONTRACT } from "../../features/installed-feature-contracts.ts"; import { ROUTE_CODECS } from "../../features/installed-feature-runtimes.tsx"; import type { ParsedRouteInput, RouteId, RouteInputResult, } from "./route-contract.ts"; export type { ParsedRouteInput, RouteId, RouteInputResult }; function codecById(codecId: string) { const codec = ROUTE_CODECS[codecId as keyof typeof ROUTE_CODECS]; if (!codec) throw new TypeError(`Unregistered route codec: ${codecId}`); return codec; } export function parseRouteInput( routeId: RouteId, rawParams: Readonly>, rawSearch: URLSearchParams, ): RouteInputResult { const runtime = ROUTE_RUNTIME_CONTRACT[routeId]; const params = codecById(runtime.paramsCodec).safeParse(rawParams); if (!params.success) { return { success: false, code: "ROUTE_PARAMS_INVALID" }; } const search = codecById(runtime.searchCodec).safeParse( searchRecord(rawSearch), ); if (!search.success) { return { success: false, code: "ROUTE_SEARCH_INVALID" }; } const parsedParams: Record = { ...params.data }; const parsedSearch: Record = { ...search.data }; return { success: true, data: Object.freeze({ routeId, params: Object.freeze(parsedParams), search: Object.freeze(parsedSearch), }), }; } export function buildRouteUrl( routeId: RouteId, input: Readonly<{ params?: Readonly>; search?: Readonly>; }> = {}, ): string { const definition = getRoute(routeId); if (definition.path === "*") { throw new TypeError("The not-found route cannot build a canonical URL"); } const runtime = ROUTE_RUNTIME_CONTRACT[routeId]; const params = codecById(runtime.paramsCodec).parse(input.params ?? {}); const search = codecById(runtime.searchCodec).parse(input.search ?? {}); const parsedParams: Record = { ...params }; const parsedSearch: Record = { ...search }; let path = definition.path; path = path.replace( /:([A-Za-z][A-Za-z0-9_]*)|\{([A-Za-z][A-Za-z0-9_]*)\}/g, ( _token: string, colonName: string | undefined, braceName: string | undefined, ) => { const name = colonName ?? braceName ?? ""; const value = parsedParams[name]; if (typeof value !== "string" && typeof value !== "number") { throw new TypeError(`Missing route path parameter: ${name}`); } return encodeURIComponent(String(value)); }, ); const query = new URLSearchParams(); for (const key of Object.keys(parsedSearch).sort((left, right) => left.localeCompare(right), )) { const value = parsedSearch[key]; if (value === undefined || value === null) continue; for (const item of Array.isArray(value) ? value : [value]) { query.append(key, String(item)); } } const serialized = query.toString(); return serialized ? `${path}?${serialized}` : path; } function searchRecord( search: URLSearchParams, ): Readonly> { const result: Record = {}; for (const key of [...new Set(search.keys())].sort((left, right) => left.localeCompare(right), )) { const values = search.getAll(key); result[key] = values.length === 1 ? values[0] : values; } return result; }