This commit is contained in:
Emmaline Autumn 2024-11-10 11:12:35 -07:00
parent d3041d789d
commit 7368533e3a
3 changed files with 237 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"deno.enable": true
}

12
deno.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "BearMetal Router",
"description": "A simple router for Deno",
"version": "0.1.0",
"stable": true,
"repository": "https://github.com/emmaos/bearmetal",
"files": [
"mod.ts",
"README.md",
"LICENSE"
]
}

222
mod.ts Normal file
View File

@ -0,0 +1,222 @@
interface RouterContext {
url: URL;
params: Record<string, string | undefined>;
state: Record<string, unknown>;
pattern: URLPattern;
request: Request;
}
type Handler = (ctx: RouterContext) => Promise<Response> | Response;
type Middleware = (ctx: RouterContext, next: () => Promise<Response>) => Promise<Response>;
interface RouteConfig {
pattern: URLPattern;
handlers: { [method: string]: Handler };
}
interface MiddlewareConfig {
pattern: URLPattern;
handler: Middleware;
}
export class Router {
private routes: RouteConfig[] = [];
private middleware: MiddlewareConfig[] = [];
route(path: string) {
path = path.startsWith('/') ? path : `/${path}`;
const pattern = new URLPattern({ pathname: path });
const routeConfig: RouteConfig = {
pattern,
handlers: {}
};
this.routes.push(routeConfig);
return {
get: (handler: Handler) => {
routeConfig.handlers['GET'] = handler;
return this;
},
post: (handler: Handler) => {
routeConfig.handlers['POST'] = handler;
return this;
},
put: (handler: Handler) => {
routeConfig.handlers['PUT'] = handler;
return this;
},
delete: (handler: Handler) => {
routeConfig.handlers['DELETE'] = handler;
return this;
},
patch: (handler: Handler) => {
routeConfig.handlers['PATCH'] = handler;
return this;
},
options: (handler: Handler) => {
routeConfig.handlers['OPTIONS'] = handler;
return this;
}
};
}
use(pathOrMiddleware: string | Middleware, middlewareOrRouter?: Middleware | Router) {
// Handle the case where only middleware is provided
if (typeof pathOrMiddleware === 'function') {
const pattern = new URLPattern({ pathname: '/*' });
this.middleware.push({
pattern,
handler: pathOrMiddleware
});
return this;
}
const path = pathOrMiddleware;
const middleware = middlewareOrRouter;
if (!middleware) {
throw new Error('Middleware or Router is required');
}
// Normalize the path
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
// Only add wildcard if there isn't already a parameter or pattern at the end
const wildcardPath = normalizedPath.includes(':') || normalizedPath.includes('*') ?
normalizedPath :
`${normalizedPath.replace(/\/+$/, '')}/*`;
if (middleware instanceof Router) {
// Merge the nested router's routes
for (const nestedRoute of middleware.routes) {
const combinedPath = this.combinePaths(path, nestedRoute.pattern.pathname);
const newPattern = new URLPattern({ pathname: combinedPath });
this.routes.push({
pattern: newPattern,
handlers: { ...nestedRoute.handlers }
});
}
// Merge the nested router's middleware
for (const nestedMiddleware of middleware.middleware) {
const combinedPath = this.combinePaths(path, nestedMiddleware.pattern.pathname);
const newPattern = new URLPattern({ pathname: combinedPath });
this.middleware.push({
pattern: newPattern,
handler: nestedMiddleware.handler
});
}
} else {
// Handle regular middleware
const pattern = new URLPattern({ pathname: wildcardPath });
this.middleware.push({
pattern,
handler: middleware
});
}
return this;
}
handle = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
const method = req.method;
// Find the matching route
const matchedRoute = this.findMatchingRoute(url);
if (!matchedRoute) {
return new Response('Not Found', { status: 404 });
}
const handler = matchedRoute.config.handlers[method];
if (!handler) {
return new Response('Method Not Allowed', { status: 405 });
}
// Get matching middleware with their parameters
const matchingMiddleware = this.getMatchingMiddleware(url);
// Create the base context object
const baseCtx: RouterContext = {
url,
params: {},
state: {},
pattern: matchedRoute.config.pattern,
request: req
};
// Combine route parameters with the base context
baseCtx.params = {
...baseCtx.params,
...matchedRoute.params
};
// Execute middleware chain
let index = 0;
const executeMiddleware = async (): Promise<Response> => {
if (index < matchingMiddleware.length) {
const { middleware, params } = matchingMiddleware[index++];
// Create a new context for each middleware with its specific parameters
const middlewareCtx: RouterContext = {
...baseCtx,
params: { ...baseCtx.params, ...params },
pattern: middleware.pattern
};
return await middleware.handler(middlewareCtx, executeMiddleware);
}
// Final handler gets the accumulated parameters
return await handler(baseCtx);
};
try {
return await executeMiddleware();
} catch (error) {
console.error('Error handling request:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
private findMatchingRoute(url: URL): { config: RouteConfig; params: Record<string, string | undefined> } | null {
for (const route of this.routes) {
const result = route.pattern.exec(url);
if (result) {
return {
config: route,
params: result.pathname.groups
};
}
}
return null;
}
private getMatchingMiddleware(url: URL): Array<{
middleware: MiddlewareConfig;
params: Record<string, string>
}> {
return this.middleware
.map(mw => {
const result = mw.pattern.exec(url);
if (result) {
return {
middleware: mw,
params: result.pathname.groups
};
}
return null;
})
.filter((item): item is {
middleware: MiddlewareConfig;
params: Record<string, string>
} => item !== null);
}
private combinePaths(basePath: string, routePath: string): string {
const normalizedBase = basePath.replace(/\/+$/, '').replace(/^\/*/, '/');
const normalizedRoute = routePath.replace(/^\/*/, '');
return `${normalizedBase}/${normalizedRoute}`;
}
}
export default Router;