Keyborg/server/src/lib/router.ts

170 lines
4.8 KiB
TypeScript

import { RouterTree } from "@lib/routerTree.ts";
import { none, Option, some } from "@shared/utils/option.ts";
import { Context } from "@lib/context.ts";
type RequestHandler<S extends string> = (
c: Context<S>,
) => Promise<Response> | Response;
export type Middleware = (
c: Context<string>,
next: () => Promise<void>,
) => Promise<Response | void> | Response | void;
type MethodHandlers<S extends string> = Partial<
Record<string, RequestHandler<S>>
>;
const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");
class HttpRouter {
routerTree = new RouterTree<MethodHandlers<any>>();
pathPreprocessor?: (path: string) => string;
middlewares: Middleware[] = [];
defaultNotFoundHandler: RequestHandler<string> = DEFAULT_NOT_FOUND_HANDLER;
setPathProcessor(processor: (path: string) => string) {
this.pathPreprocessor = processor;
}
use(mw: Middleware): HttpRouter {
this.middlewares.push(mw);
return this;
}
add<S extends string>(
path: S,
method: string,
handler: RequestHandler<S>,
): HttpRouter;
add<S extends string>(
path: S[],
method: string,
handler: RequestHandler<string>,
): HttpRouter;
add(
path: string | string[],
method: string,
handler: RequestHandler<string>,
): 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<string> = {};
mth[method] = handler;
this.routerTree.add(p, mth);
},
);
}
return this;
}
// Overload signatures for 'get'
get<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
get<S extends string>(
path: S[],
handler: RequestHandler<string>,
): HttpRouter;
// Non-generic implementation for 'get'
get(path: string | string[], handler: RequestHandler<string>): HttpRouter {
if (Array.isArray(path)) {
return this.add(path, "GET", handler);
}
return this.add(path, "GET", handler);
}
post<S extends string>(path: S, handler: RequestHandler<S>): HttpRouter;
post<S extends string>(
path: string[],
handler: RequestHandler<string>,
): HttpRouter;
post(path: string | string[], handler: RequestHandler<string>): HttpRouter {
if (Array.isArray(path)) {
return this.add(path, "POST", handler);
}
return this.add(path, "POST", handler);
}
async handleRequest(
req: Request,
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
): Promise<Response> {
const c = new Context(req, connInfo, {});
const path = this.pathPreprocessor
? this.pathPreprocessor(c.path)
: c.path;
let params: 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<S extends string>(
middlewares: Middleware[],
handler: RequestHandler<S>,
c: Context<S>,
) {
let currentIndex = -1;
const dispatch = async (index: number): Promise<void> => {
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;
}
}
export type ExtractRouteParams<T extends string> = T extends string
? T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${infer _Start}:${infer Param}` ? Param
: T extends `${infer _Start}*` ? "restOfThePath"
: never
: never;
export type Params<Keys extends string> = {
[K in Keys]: string;
};
export default HttpRouter;