import type { HousekeepingDomainId } from "../../migration/types"; import type { CanonicalHousekeepingHref } from "../contracts"; import type { HousekeepingRegistry } from "../registry"; export interface HousekeepingRouteMatch { routeId: string; domain: HousekeepingDomainId; params: Readonly>; canonicalHref: CanonicalHousekeepingHref; } interface ParsedCanonicalPath { segments: readonly string[]; } interface RoutePatternSegment { literal: string | null; parameterName: string | null; } interface RouteCandidate { routeId: string; domain: HousekeepingDomainId; patternSegments: readonly RoutePatternSegment[]; } export function matchHousekeepingRoute( registry: HousekeepingRegistry, canonicalPath: string, ): HousekeepingRouteMatch | null { const path = parseCanonicalPath(canonicalPath); if (!path) return null; const candidates = registry.domains .flatMap((domain) => domain.routes.flatMap((route) => { if (!routeBelongsToDomain(route.href, domain.canonicalHref)) return []; const patternSegments = parseRoutePattern(route.href); return patternSegments ? [{ routeId: route.id, domain: domain.id, patternSegments }] : []; }), ) .sort(compareSpecificity); for (const candidate of candidates) { const params = matchSegments(candidate, path); if (!params) continue; return { routeId: candidate.routeId, domain: candidate.domain, params: Object.freeze(params), canonicalHref: canonicalPath as CanonicalHousekeepingHref, }; } return null; } function parseCanonicalPath(canonicalPath: string): ParsedCanonicalPath | null { if ( canonicalPath !== canonicalPath.trim() || !canonicalPath.startsWith("/ase") || canonicalPath.includes("?") || canonicalPath.includes("#") || canonicalPath.includes("\\") || canonicalPath.endsWith("/") ) { return null; } const rawSegments = canonicalPath.slice(1).split("/"); if (rawSegments[0] !== "ase" || rawSegments.some((segment) => !segment)) { return null; } const segments: string[] = []; for (const segment of rawSegments) { const decoded = decodeCanonicalSegment(segment); if (decoded === null) return null; segments.push(decoded); } return { segments }; } function parseRoutePattern( routeHref: CanonicalHousekeepingHref, ): readonly RoutePatternSegment[] | null { const segments: RoutePatternSegment[] = []; for (const segment of routeHref.slice(1).split("/")) { if (segment.startsWith(":")) { const parameterName = segment.slice(1); if (!parameterName) return null; segments.push({ literal: null, parameterName }); continue; } const literal = decodeCanonicalSegment(segment); if (literal === null) return null; segments.push({ literal, parameterName: null }); } return segments; } function decodeCanonicalSegment(segment: string): string | null { let decoded: string; try { decoded = decodeURIComponent(segment); } catch { return null; } return !decoded || decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\") ? null : decoded; } function compareSpecificity( left: RouteCandidate, right: RouteCandidate, ): number { const sharedLength = Math.min( left.patternSegments.length, right.patternSegments.length, ); for (let index = 0; index < sharedLength; index += 1) { const leftDynamic = left.patternSegments[index]?.parameterName !== null; const rightDynamic = right.patternSegments[index]?.parameterName !== null; if (leftDynamic === rightDynamic) continue; return leftDynamic ? 1 : -1; } return left.patternSegments.length - right.patternSegments.length; } function routeBelongsToDomain( routeHref: CanonicalHousekeepingHref, domainHref: CanonicalHousekeepingHref, ): boolean { return ( routeHref === domainHref || (domainHref !== "/ase" && routeHref.startsWith(`${domainHref}/`)) || (domainHref === "/ase" && routeHref === "/ase") ); } function matchSegments( candidate: RouteCandidate, path: ParsedCanonicalPath, ): Record | null { if (candidate.patternSegments.length !== path.segments.length) return null; const params: Record = {}; for (const [index, patternSegment] of candidate.patternSegments.entries()) { const pathSegment = path.segments[index] ?? ""; if (patternSegment.parameterName === null) { if (patternSegment.literal !== pathSegment) return null; continue; } if (patternSegment.parameterName in params) return null; params[patternSegment.parameterName] = pathSegment; } return params; }