From 7368533e3ae2b3fd289add01f699836002b027e0 Mon Sep 17 00:00:00 2001 From: Emma Short Date: Sun, 10 Nov 2024 11:12:35 -0700 Subject: [PATCH] init --- .vscode/settings.json | 3 + deno.json | 12 +++ mod.ts | 222 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 deno.json create mode 100644 mod.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b9fb22 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..cd278d8 --- /dev/null +++ b/deno.json @@ -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" + ] +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..1451f3e --- /dev/null +++ b/mod.ts @@ -0,0 +1,222 @@ +interface RouterContext { + url: URL; + params: Record; + state: Record; + pattern: URLPattern; + request: Request; +} + +type Handler = (ctx: RouterContext) => Promise | Response; +type Middleware = (ctx: RouterContext, next: () => Promise) => Promise; + +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 => { + 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 => { + 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 } | 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 + }> { + 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 + } => item !== null); + } + + private combinePaths(basePath: string, routePath: string): string { + const normalizedBase = basePath.replace(/\/+$/, '').replace(/^\/*/, '/'); + const normalizedRoute = routePath.replace(/^\/*/, ''); + return `${normalizedBase}/${normalizedRoute}`; + } +} + +export default Router; \ No newline at end of file