From 35d8622597a8cf1581eebc830e7f498a80600f0a Mon Sep 17 00:00:00 2001 From: Seb Ringrose Date: Mon, 3 Jul 2023 21:58:52 +0100 Subject: [PATCH] feat: /added/:params/to/routes --- lib/Server.ts | 49 ++++++++++++++++++++++++------------- lib/utils/Cascade.ts | 8 +++--- lib/utils/Router.ts | 39 +++++++++++++++++++++++++---- tests/server_test.ts | 12 +++++++++ tests/utils/Cascade_test.ts | 6 +++-- tests/utils/Router_test.ts | 11 ++++++++- 6 files changed, 96 insertions(+), 29 deletions(-) diff --git a/lib/Server.ts b/lib/Server.ts index 222eaf14..3cfb523a 100644 --- a/lib/Server.ts +++ b/lib/Server.ts @@ -1,30 +1,46 @@ import { Server as stdServer } from "https://deno.land/std@0.174.0/http/server.ts" -import { Router } from "./utils/Router.ts" +import { Router, _Route } from "./utils/Router.ts" import { Cascade, PromiseMiddleware } from "./utils/Cascade.ts" -import { Middleware, Route } from "./types.ts" +import { Middleware } from "./types.ts" export class RequestContext { - server: Server - request: Request + url: URL + _route: _Route | undefined state: Record + params: Record = {} - constructor(server: Server, request: Request, state?: Record) { - this.server = server - this.request = request - this.state = state - ? state - : {} + constructor( + public server: Server, + public request: Request, + state?: Record + ) { + this.url = new URL(request.url) + this.state = state ? state : {} + } + + get route () { + return this._route + } + + set route (r: _Route | undefined) { + this._route = r; + if (r?.params) { + const pathBits = this.url.pathname.split("/") + for (const param in r.params) { + this.params[param] = pathBits[r.params[param]] + } + } } } export class Server extends Router { stdServer: stdServer | undefined port = 7777 - hostname = "0.0.0.0" + hostname = "127.0.0.1" middleware: PromiseMiddleware[] = [] routers: Router[] = [] - public get allRoutes(): Route[] { + public get allRoutes(): _Route[] { return [ this, ...this.routers].map(router => router.routes).flat() } @@ -93,14 +109,13 @@ export class Server extends Router { */ async requestHandler(request: Request): Promise { const ctx: RequestContext = new RequestContext(this, request) - const requestURL = new URL(ctx.request.url) - const route = this.allRoutes.find(route => - route.path === requestURL.pathname && + ctx.route = this.allRoutes.find(route => + route.regexPath.test(ctx.url.pathname) && route.method === request.method ) - - return await new Cascade(ctx, route).start() + + return await new Cascade(ctx).start() } /** diff --git a/lib/utils/Cascade.ts b/lib/utils/Cascade.ts index 6c09fc14..6444fdb4 100644 --- a/lib/utils/Cascade.ts +++ b/lib/utils/Cascade.ts @@ -1,5 +1,5 @@ import { RequestContext } from "../Server.ts" -import { Middleware, Result, Next, Route } from "../types.ts" +import { Middleware, Result, Next } from "../types.ts" export type PromiseMiddleware = (ctx: RequestContext, next: Next) => Promise @@ -11,9 +11,9 @@ export class Cascade { called = 0 toCall: PromiseMiddleware[] - constructor(public ctx: RequestContext, private route?: Route) { - this.toCall = this.route - ? [...this.ctx.server.middleware, ...this.route.middleware as PromiseMiddleware[], this.route.handler as PromiseMiddleware] + constructor(public ctx: RequestContext) { + this.toCall = this.ctx.route + ? [...this.ctx.server.middleware, ...this.ctx.route.middleware as PromiseMiddleware[], this.ctx.route.handler as PromiseMiddleware] : [...this.ctx.server.middleware] } diff --git a/lib/utils/Router.ts b/lib/utils/Router.ts index 29a28cfd..4a1ef292 100644 --- a/lib/utils/Router.ts +++ b/lib/utils/Router.ts @@ -1,8 +1,37 @@ import { Middleware, Handler, Route } from "../types.ts" import { Cascade } from "./Cascade.ts" +export class _Route implements Route { + path: `/${string}` + params: Record = {} + regexPath: RegExp + method?: "GET" | "POST" | "PUT" | "DELETE" + middleware?: Middleware[] | Middleware + handler: Handler + + constructor(routeObj: Route) { + if (!routeObj.path) throw new Error("Route is missing path") + if (!routeObj.handler) throw new Error("Route is missing handler") + + this.path = routeObj.path + this.path.split("/").forEach((str, i) => { + if (str[0] === ":") this.params[str.slice(1)] = i + }); + this.regexPath = this.params + ? new RegExp(this.path.replaceAll(/(?<=\/):(.)*?(?=\/|$)/g, "(.)*")) + : new RegExp(this.path) + + this.method = routeObj.method || "GET" + this.handler = Cascade.promisify(routeObj.handler!) as Handler + this.middleware = [routeObj.middleware] + .flat() + .filter(Boolean) + .map((mware) => Cascade.promisify(mware!)) + } +} + export class Router { - constructor(public routes: Route[] = []) {} + constructor(public routes: _Route[] = []) {} static applyDefaults(routeObj: Partial): Route { if (!routeObj.path) throw new Error("Route is missing path") @@ -11,9 +40,9 @@ export class Router { routeObj.method = routeObj.method || "GET" routeObj.handler = Cascade.promisify(routeObj.handler!) as Handler routeObj.middleware = [routeObj.middleware] - .flat() - .filter(Boolean) - .map((mware) => Cascade.promisify(mware!)) + .flat() + .filter(Boolean) + .map((mware) => Cascade.promisify(mware!)) return routeObj as Route } @@ -45,7 +74,7 @@ export class Router { throw new Error(`Route with path ${routeObj.path} already exists!`) } - const fullRoute = Router.applyDefaults(routeObj) + const fullRoute = new _Route(routeObj as Route) this.routes.push(fullRoute) return fullRoute diff --git a/tests/server_test.ts b/tests/server_test.ts index 29756d21..5b882e2c 100644 --- a/tests/server_test.ts +++ b/tests/server_test.ts @@ -85,4 +85,16 @@ Deno.test("SERVER", async (t) => { assert(body["middleware1"] && body["middleware2"] && body["middleware3"]) }) + + await t.step("params discovered in RequestContext creation", async () => { + const newServer = new Server(); + + newServer.addRoute("/hello/:id/world/:name", (ctx) => { + return new Response(JSON.stringify({ id: ctx.params["id"], name: ctx.params["name"] })) + }) + + const res = await newServer.requestHandler(new Request("http://localhost:7777/hello/123/world/bruno")) + const json = await res.json() + assert(json.id === "123" && json.name === "bruno") + }) }) diff --git a/tests/utils/Cascade_test.ts b/tests/utils/Cascade_test.ts index d81bab3a..ee9585d1 100644 --- a/tests/utils/Cascade_test.ts +++ b/tests/utils/Cascade_test.ts @@ -1,6 +1,7 @@ import { assert } from "https://deno.land/std@0.174.0/testing/asserts.ts" import { Server, RequestContext } from "../../lib/Server.ts" import { Cascade } from "../../lib/utils/Cascade.ts" +import { _Route } from "../../lib/utils/Router.ts" import { testMiddleware1, testMiddleware2, @@ -11,8 +12,7 @@ import { Deno.test("UTIL: Cascade", async (t) => { const testServer = new Server() const testContext = new RequestContext(testServer, new Request("http://localhost")) - - const cascade = new Cascade(testContext, { + testContext._route = new _Route({ path: "/", middleware: [ testMiddleware1, @@ -22,6 +22,8 @@ Deno.test("UTIL: Cascade", async (t) => { ], handler: testHandler }) + + const cascade = new Cascade(testContext) const result = await cascade.start() diff --git a/tests/utils/Router_test.ts b/tests/utils/Router_test.ts index 552457b2..1ae2a3af 100644 --- a/tests/utils/Router_test.ts +++ b/tests/utils/Router_test.ts @@ -30,7 +30,8 @@ Deno.test("ROUTER", async (t) => { await t.step ("routers on server can be subsequently editted", () => { const server = new Server() - const aRouter = new Router([ + const aRouter = new Router() + aRouter.addRoutes([ { path: "/route", handler: testHandler }, { path: "/route2", handler: testHandler }, { path: "/route3", handler: testHandler } @@ -70,4 +71,12 @@ Deno.test("ROUTER", async (t) => { assert(putRoute.method === "PUT") assert(deleteRoute.method === "DELETE") }) + + await t.step("Params correctly stored", () => { + const router = new Router() + router.addRoute("/hello/:id/world/:name", () => new Response("Hi!")) + + assert(router.routes[0].params["id"] === 2) + assert(router.routes[0].params["name"] === 4) + }) })