import type { ConnInfo } from "https://deno.land/std@0.128.0/http/server.ts";
type HandlerContext<T = unknown> = T & ConnInfo;
export type Handler<T = unknown> = (
req: Request,
ctx: HandlerContext<T>,
) => Response | Promise<Response>;
export type ErrorHandler<T = unknown> = (
req: Request,
ctx: HandlerContext<T>,
err: unknown,
) => Response | Promise<Response>;
export type UnknownMethodHandler<T = unknown> = (
req: Request,
ctx: HandlerContext<T>,
knownMethods: string[],
) => Response | Promise<Response>;
export type MatchHandler<T = unknown> = (
req: Request,
ctx: HandlerContext<T>,
match: Record<string, string>,
) => Response | Promise<Response>;
export type Routes<T = {}> = Record<string, MatchHandler<T>>;
export function defaultOtherHandler(_req: Request): Response {
return new Response(null, {
status: 404,
});
}
export function defaultErrorHandler(
_req: Request,
_ctx: HandlerContext,
err: unknown,
): Response {
console.error(err);
return new Response(null, {
status: 500,
});
}
export function defaultUnknownMethodHandler(
_req: Request,
_ctx: HandlerContext,
knownMethods: string[],
): Response {
return new Response(null, {
status: 405,
headers: {
Accept: knownMethods.join(", "),
},
});
}
export const METHODS = [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"OPTIONS",
"PATCH",
] as const;
const methodRegex = new RegExp(`(?<=^(?:${METHODS.join("|")}))@`);
export function router<T = unknown>(
routes: Routes<T>,
other: Handler<T> = defaultOtherHandler,
error: ErrorHandler<T> = defaultErrorHandler,
unknownMethod: UnknownMethodHandler<T> = defaultUnknownMethodHandler,
): Handler<T> {
const internalRoutes: Record<string, { pattern: URLPattern, methods: Record<string, MatchHandler<T>> }> = {};
for (const [route, handler] of Object.entries(routes)) {
let [methodOrPath, path] = route.split(methodRegex);
let method = methodOrPath;
if (!path) {
path = methodOrPath;
method = "any";
}
const r = internalRoutes[path] ?? {
pattern: new URLPattern({ pathname: path }),
methods: {}
};
r.methods[method] = handler;
internalRoutes[path] = r;
}
return async (req, ctx) => {
try {
for (const { pattern, methods } of Object.values(internalRoutes)) {
const res = pattern.exec(req.url);
if (res !== null) {
for (const [method, handler] of Object.entries(methods)) {
if (req.method === method) {
return await handler(
req,
ctx,
res.pathname.groups,
);
}
}
if (methods["any"]) {
return await methods["any"](
req,
ctx,
res.pathname.groups,
);
} else {
return await unknownMethod(
req,
ctx,
Object.keys(methods),
);
}
}
}
return await other(req, ctx);
} catch (err) {
return error(req, ctx, err);
}
};
}