first iteration of v.3 router
This commit is contained in:
222
router.new.ts
Normal file
222
router.new.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import type { RouterContext as oldContext } from "@bearmetal/router/types";
|
||||
import { joinPaths } from "./util/join.ts";
|
||||
|
||||
type RouterContext = Omit<oldContext, "pattern">;
|
||||
type Handler = (
|
||||
req: Request,
|
||||
ctx: RouterContext,
|
||||
next: () => Promise<void | Response> | null,
|
||||
) => Promise<Response>;
|
||||
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<string, RouteConfig> = 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<Response> => {
|
||||
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;
|
Reference in New Issue
Block a user