import { RouterTree } from "@lib/routerTree.ts"; import { none, Option, some } from "@shared/utils/option.ts"; import { Context } from "@lib/context.ts"; type RequestHandler = ( c: Context, ) => Promise | Response; export type Middleware = ( c: Context, next: () => Promise, ) => Promise | Response | void; type MethodHandlers = Partial< Record> >; const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found"); class HttpRouter { routerTree = new RouterTree>(); pathPreprocessor?: (path: string) => string; middlewares: Middleware[] = []; defaultNotFoundHandler: RequestHandler = DEFAULT_NOT_FOUND_HANDLER; setPathProcessor(processor: (path: string) => string) { this.pathPreprocessor = processor; } use(mw: Middleware): HttpRouter { this.middlewares.push(mw); return this; } add( path: S, method: string, handler: RequestHandler, ): HttpRouter; add( path: S[], method: string, handler: RequestHandler, ): HttpRouter; add( path: string | string[], method: string, handler: RequestHandler, ): HttpRouter { const paths = Array.isArray(path) ? path : [path]; for (const p of paths) { this.routerTree.getHandler(p).match( (mth) => { mth[method] = handler; }, () => { const mth: MethodHandlers = {}; mth[method] = handler; this.routerTree.add(p, mth); }, ); } return this; } // Overload signatures for 'get' get(path: S, handler: RequestHandler): HttpRouter; get( path: S[], handler: RequestHandler, ): HttpRouter; // Non-generic implementation for 'get' get(path: string | string[], handler: RequestHandler): HttpRouter { if (Array.isArray(path)) { return this.add(path, "GET", handler); } return this.add(path, "GET", handler); } post(path: S, handler: RequestHandler): HttpRouter; post( path: string[], handler: RequestHandler, ): HttpRouter; post(path: string | string[], handler: RequestHandler): HttpRouter { if (Array.isArray(path)) { return this.add(path, "POST", handler); } return this.add(path, "POST", handler); } async handleRequest( req: Request, connInfo: Deno.ServeHandlerInfo, ): Promise { const c = new Context(req, connInfo, {}); const path = this.pathPreprocessor ? this.pathPreprocessor(c.path) : c.path; let params: string[] = []; const handler = this.routerTree .find(path) .andThen((routeMatch) => { const { value: handlers, params: paramsMatched } = routeMatch; params = paramsMatched; const handler = handlers[req.method]; return handler ? some(handler) : none; }) .unwrapOrElse(() => this.defaultNotFoundHandler); const cf = await this.executeMiddlewareChain( this.middlewares, handler, Context.setParams(c, params), ); return cf.res; } private async executeMiddlewareChain( middlewares: Middleware[], handler: RequestHandler, c: Context, ) { let currentIndex = -1; const dispatch = async (index: number): Promise => { currentIndex = index; if (index < middlewares.length) { const middleware = middlewares[index]; const result = await middleware(c, () => dispatch(index + 1)); if (result !== undefined) { c.res = await Promise.resolve(result); } } else { const res = await handler(c); c.res = res; } }; await dispatch(0); return c; } private setParams(path: string, params: string[]): Params { path.split("/").filter((segmet) => segmet.startsWith(":")); } } export type ExtractRouteParams = T extends string ? T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Param | ExtractRouteParams : T extends `${infer _Start}:${infer Param}` ? Param : T extends `${infer _Start}*` ? "restOfThePath" : never : never; export type Params = { [K in Keys]: string; }; export default HttpRouter;