import type { RouterContext as oldContext } from "@bearmetal/router/types"; import { joinPaths } from "./util/join.ts"; type RouterContext = Omit; type Handler = ( req: Request, ctx: RouterContext, next: () => Promise | null, ) => Promise; type RouteConfigurator = { get(handler: Handler): RouteConfigurator; post(handler: Handler): RouteConfigurator; put(handler: Handler): RouteConfigurator; delete(handler: Handler): RouteConfigurator; patch(handler: Handler): RouteConfigurator; options(handler: Handler): RouteConfigurator; use(handler: Handler | Router): RouteConfigurator; }; type RouteConfig = { handlers: { [method: string]: Handler[] }; pattern: URLPattern; }; const GET = "GET"; const POST = "POST"; const PUT = "PUT"; const PATCH = "PATCH"; const DELETE = "DELETE"; const OPTIONS = "OPTIONS"; const _use = "_use"; const methods = [GET, POST, PUT, PATCH, DELETE, OPTIONS, _use]; export class Router { protected routes: Map = new Map(); /** * @description defines a new route * @param path the path to match, uses the same syntax as the URLPattern constructor * @returns a RouteConfigurator object * * @example * ```ts * router.route('/users') * .get((req, ctx) => { * return new Response('GET /users'); * }) * .post((req, ctx) => { * return new Response('POST /users'); * }); * ``` */ route(path: string): RouteConfigurator { path = fixPath(path); const routeConfig = this.getOrCreateConfig(path); const configurator = { get: (handler: Handler) => { this.getOrCreateConfigHandlers(GET, routeConfig).push(handler); return configurator; }, post: (handler: Handler) => { this.getOrCreateConfigHandlers(POST, routeConfig).push(handler); return configurator; }, put: (handler: Handler) => { this.getOrCreateConfigHandlers(PUT, routeConfig).push(handler); return configurator; }, patch: (handler: Handler) => { this.getOrCreateConfigHandlers(PATCH, routeConfig).push(handler); return configurator; }, delete: (handler: Handler) => { this.getOrCreateConfigHandlers(DELETE, routeConfig).push(handler); return configurator; }, options: (handler: Handler) => { this.getOrCreateConfigHandlers(OPTIONS, routeConfig).push(handler); return configurator; }, use: (handler: Handler | Router) => { if (handler instanceof Router) { this.resolveRouterHandlerStack(path, handler); return configurator; } this.getOrCreateConfigHandlers(_use, routeConfig).push(handler); return configurator; }, }; return configurator; } get(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(GET, config).push(handler); } post(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(POST, config).push(handler); } put(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(PUT, config).push(handler); } patch(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(PATCH, config).push(handler); } delete(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(DELETE, config).push(handler); } options(path: string, handler: Handler) { path = fixPath(path); const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(OPTIONS, config).push(handler); } use(path: string, handler: Handler | Router) { path = fixPath(path); if (handler instanceof Router) { return this.resolveRouterHandlerStack(path, handler); } const config = this.getOrCreateConfig(path); this.getOrCreateConfigHandlers(_use, config).push(handler); } private getOrCreateConfig(path: string): RouteConfig { let config = this.routes.get(path); if (!config) { config = { handlers: {}, pattern: new URLPattern({ pathname: path }), }; } return config; } private getOrCreateConfigHandlers(method: string, config: RouteConfig) { config.handlers[method] = config.handlers[method] ?? []; return config.handlers[method]; } resolveRouterHandlerStack(path: string, router: Router): void { for (const route of router.rawRoutes) { const p = joinPaths(path, route[0]); const thisConfig = this.getOrCreateConfig(p); const thatConfig = route[1]; for (const method of methods) { if (!thatConfig.handlers[method]) continue; thisConfig.handlers[method] = (thisConfig.handlers[method] ?? []) .concat(thatConfig.handlers[method]); } } } get rawRoutes() { return this.routes.entries(); } get handle() { return this.handler; } async handler(req: Request) { const url = new URL(req.url); const method = req.method; const matchingRoutes = this.findMatchingRoutes(url).filter((r) => Object.hasOwn(r.config.handlers, method) || Object.hasOwn(r.config.handlers, _use) ); const middlewareStack = matchingRoutes.flatMap((r) => r.config.handlers[method] ); const ctx: RouterContext = { url, params: matchingRoutes.reduce((a, b) => ({ ...a, ...b.params }), {}), state: {}, request: req, }; let index = 0; const executeMiddleware = async (): Promise => { if (index < middlewareStack.length) { const res = await middlewareStack[index++](req, ctx, executeMiddleware); if (res instanceof Response) return res; } return new Response("End of stack", { status: 501 }); }; try { return await executeMiddleware(); } catch { return new Response("Internal Server Error", { status: 500 }); } } private findMatchingRoutes(url: URL) { return this.routes.values().map((route) => { const result = route.pattern.exec(url); if (result) { return { config: route, params: result.pathname.groups, }; } }).filter((r) => !!r).toArray(); } } function fixPath(path: string) { path = path.startsWith("/") ? path : `/${path}`; return path; } export default Router;