173 lines
4.9 KiB
TypeScript
173 lines
4.9 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: 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;
|
|
}
|
|
|
|
private setParams(path: string, params: string[]): Params<string> {
|
|
path.split("/").filter((segmet) => segmet.startsWith(":"));
|
|
}
|
|
}
|
|
|
|
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;
|