import { http } from "./deps.ts";import { requestData, NO_MATCH } from "./http.ts";
import type { RouterRequest, RouterShape, Handler } from "./client.ts";
export interface Stack<S extends RouterShape = RouterShape> { (req: RouterRequest<S>, connInfo: http.ConnInfo): Promise<Response>; readonly routes: S;}
const nextPathGroupName = "__nextPath";
export function stack<S extends RouterShape>(routes: S): Stack<S> { for (const [k, _] of Object.entries(routes)) { const split = k.split("/").filter(v => !!v);
if (split[split.length-1] === "*") { split.pop(); }
for (const s of split) { if ( !s.match(/^:[a-zA-Z_$][a-zA-Z_$0-9]$/) && !s.match(/^[^:*?(){}]*$/) ) { throw new SyntaxError( `"${k}" isn't a valid stack route. Stack routes only support basic path segments and named path segments (groups). Non-trailing wildcards, RegExp, optionals, and other advanced URLPattern syntax is not supported`, ); } } }
const handlers: Record<string, Handler | Handler[]> = {}; for (const k of Object.keys(routes)) { const v = routes[k]; if (v && typeof v === "object" && !Array.isArray(v)) { handlers[k] = stack(v); } else if (v) { handlers[k] = v; } }
const paths = Object.keys(handlers); const sortedPaths: string[] = paths.sort((a, b) => { if (a === b) { return 0; } if (a === "*" || a === "/*") { return 1; } if (b === "*" || b === "/*") { return -1; }
if (a.endsWith("/*")) { a = a.slice(0, a.length - 2); } if (b.endsWith("/*")) { b = b.slice(0, b.length - 2); } const la = a.split("/").filter(v => !!v).length; const lb = b.split("/").filter(v => !!v).length; if (la !== lb) { return lb - la; }
return paths.indexOf(a) - paths.indexOf(b); });
const patterns = new Map<URLPattern, Handler | Handler[]>(); for (const op of sortedPaths) { let p = "/" + op.split("/").filter(v => !!v).join("/");
if (p.endsWith("/*")) { p = p.slice(0, p.length - 2); } p = `${p}/:${nextPathGroupName}*/*?`; patterns.set(new URLPattern(p, "http://_._"), handlers[op]); }
const stackHandler = async ( req: Request, conn: http.ConnInfo, ): Promise<Response> => { const data = requestData(req); if (data instanceof Response) { return data; }
for (const [pattern, handler] of patterns.entries()) { const match = pattern.exec(data.path, "http://_._"); if (!match) { continue; }
const groups = { ...data.groups, ...match.pathname.groups }; const path = `/${groups[nextPathGroupName] || ""}`; delete groups[nextPathGroupName];
const oPath = data.path; const oGroups = data.groups;
Object.assign(data, { path, groups });
try { if (Array.isArray(handler)) { for (const h of handler) { try { return await h(req, conn); } catch (e) { if (e === NO_MATCH) { continue; } } } throw NO_MATCH; } return await handler(req, conn); } catch (e) { if (e === NO_MATCH) { continue; } throw e; } finally { Object.assign(req, { path: oPath, groups: oGroups }); } }
throw NO_MATCH; };
return Object.assign(stackHandler, { routes });}