Skip to content

Commit

Permalink
feat: /added/:params/to/routes
Browse files Browse the repository at this point in the history
  • Loading branch information
sejori committed Jul 3, 2023
1 parent 403df1b commit 35d8622
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 29 deletions.
49 changes: 32 additions & 17 deletions lib/Server.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
params: Record<string, unknown> = {}

constructor(server: Server, request: Request, state?: Record<string, unknown>) {
this.server = server
this.request = request
this.state = state
? state
: {}
constructor(
public server: Server,
public request: Request,
state?: Record<string, unknown>
) {
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()
}

Expand Down Expand Up @@ -93,14 +109,13 @@ export class Server extends Router {
*/
async requestHandler(request: Request): Promise<Response> {
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()
}

/**
Expand Down
8 changes: 4 additions & 4 deletions lib/utils/Cascade.ts
Original file line number Diff line number Diff line change
@@ -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<Result>

Expand All @@ -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]
}

Expand Down
39 changes: 34 additions & 5 deletions lib/utils/Router.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = {}
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>): Route {
if (!routeObj.path) throw new Error("Route is missing path")
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
6 changes: 4 additions & 2 deletions tests/utils/Cascade_test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -22,6 +22,8 @@ Deno.test("UTIL: Cascade", async (t) => {
],
handler: testHandler
})

const cascade = new Cascade(testContext)

const result = await cascade.start()

Expand Down
11 changes: 10 additions & 1 deletion tests/utils/Router_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
})
})

0 comments on commit 35d8622

Please sign in to comment.