Compare commits
3 Commits
0.2.4
...
f0ea182d2f
Author | SHA1 | Date | |
---|---|---|---|
f0ea182d2f | |||
0b4f504ba2 | |||
1d3be78917 |
0
.vscode/settings.json
vendored
Executable file → Normal file
0
.vscode/settings.json
vendored
Executable file → Normal file
2
deno.json
Executable file → Normal file
2
deno.json
Executable file → Normal file
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@bearmetal/router",
|
"name": "@bearmetal/router",
|
||||||
"description": "A simple router for Deno",
|
"description": "A simple router for Deno",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"stable": true,
|
"stable": true,
|
||||||
"files": [
|
"files": [
|
||||||
"mod.ts",
|
"mod.ts",
|
||||||
|
7
deno.lock
generated
Executable file → Normal file
7
deno.lock
generated
Executable file → Normal file
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@std/assert@^1.0.7": "1.0.7",
|
"jsr:@std/assert@^1.0.7": "1.0.7",
|
||||||
|
"jsr:@std/async@^1.0.8": "1.0.13",
|
||||||
"jsr:@std/data-structures@^1.0.4": "1.0.4",
|
"jsr:@std/data-structures@^1.0.4": "1.0.4",
|
||||||
"jsr:@std/fs@^1.0.5": "1.0.5",
|
"jsr:@std/fs@^1.0.5": "1.0.5",
|
||||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||||
@@ -16,6 +17,9 @@
|
|||||||
"jsr:@std/internal"
|
"jsr:@std/internal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@std/async@1.0.13": {
|
||||||
|
"integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96"
|
||||||
|
},
|
||||||
"@std/data-structures@1.0.4": {
|
"@std/data-structures@1.0.4": {
|
||||||
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
|
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
|
||||||
},
|
},
|
||||||
@@ -35,6 +39,7 @@
|
|||||||
"integrity": "ca1368d720b183f572d40c469bb9faf09643ddd77b54f8b44d36ae6b94940576",
|
"integrity": "ca1368d720b183f572d40c469bb9faf09643ddd77b54f8b44d36ae6b94940576",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/assert",
|
"jsr:@std/assert",
|
||||||
|
"jsr:@std/async",
|
||||||
"jsr:@std/data-structures",
|
"jsr:@std/data-structures",
|
||||||
"jsr:@std/fs",
|
"jsr:@std/fs",
|
||||||
"jsr:@std/internal",
|
"jsr:@std/internal",
|
||||||
|
0
fileRouterTest/test.ts
Executable file → Normal file
0
fileRouterTest/test.ts
Executable file → Normal file
0
file_router.ts
Executable file → Normal file
0
file_router.ts
Executable file → Normal file
293
router.new.test.ts
Normal file
293
router.new.test.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// deno-lint-ignore-file require-await
|
||||||
|
import { assertEquals } from "@std/assert";
|
||||||
|
import { beforeEach, describe, it } from "@std/testing/bdd";
|
||||||
|
import Router from "./router.new.ts";
|
||||||
|
|
||||||
|
describe("Router", () => {
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
router = new Router();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Basic Routing", () => {
|
||||||
|
it("should handle basic GET request", async () => {
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => new Response("test response"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 200);
|
||||||
|
assertEquals(await res.text(), "test response");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 for non-existent route", async () => {
|
||||||
|
const req = new Request("http://localhost/nonexistent", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 405 for unsupported method", async () => {
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 405);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Route Parameters", () => {
|
||||||
|
it("should handle route parameters", async () => {
|
||||||
|
router.route("/users/:id")
|
||||||
|
.get(async (_, ctx) => new Response(ctx.params.id));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/users/123", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(await res.text(), "123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple route parameters", async () => {
|
||||||
|
router.route("/users/:userId/posts/:postId")
|
||||||
|
.get(async (_, ctx) => {
|
||||||
|
return new Response(JSON.stringify(ctx.params));
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/users/123/posts/456", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
const params = JSON.parse(await res.text());
|
||||||
|
assertEquals(params.userId, "123");
|
||||||
|
assertEquals(params.postId, "456");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Middleware", () => {
|
||||||
|
it("should execute global middleware", async () => {
|
||||||
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
|
router.use(async (_, _ctx, next) => {
|
||||||
|
middlewareExecuted = true;
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.handle(req);
|
||||||
|
assertEquals(middlewareExecuted, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should execute path-specific middleware", async () => {
|
||||||
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
|
router.use("/test", async (_, _ctx, next) => {
|
||||||
|
middlewareExecuted = true;
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.handle(req);
|
||||||
|
assertEquals(middlewareExecuted, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle middleware parameters", async () => {
|
||||||
|
const capturedParams: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
router.use("/:version/.*", async (_, ctx, next) => {
|
||||||
|
capturedParams.version = ctx.params.version;
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/:version/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/v1/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.handle(req);
|
||||||
|
assertEquals(capturedParams.version, "v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should execute middleware in correct order", async () => {
|
||||||
|
const order: number[] = [];
|
||||||
|
|
||||||
|
router.use(async (_, _ctx, next) => {
|
||||||
|
order.push(1);
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use("/test", async (_, _ctx, next) => {
|
||||||
|
order.push(2);
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => {
|
||||||
|
order.push(3);
|
||||||
|
return new Response("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.handle(req);
|
||||||
|
assertEquals(order, [1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Nested Routers", () => {
|
||||||
|
it("should handle nested routes", async () => {
|
||||||
|
const apiRouter = new Router();
|
||||||
|
apiRouter.route("/test")
|
||||||
|
.get(async () => new Response("api test"));
|
||||||
|
|
||||||
|
router.use("/api", apiRouter);
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/api/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(await res.text(), "api test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nested routes with parameters", async () => {
|
||||||
|
const apiRouter = new Router();
|
||||||
|
apiRouter.route("/users/:id")
|
||||||
|
.get(async (_, ctx) => new Response(ctx.params.id));
|
||||||
|
|
||||||
|
router.use("/api/:version", apiRouter);
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/api/v1/users/123", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(await res.text(), "123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle nested middleware", async () => {
|
||||||
|
const apiRouter = new Router();
|
||||||
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
|
apiRouter.use(async (_, _ctx, next) => {
|
||||||
|
middlewareExecuted = true;
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.route("/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
router.use("/api", apiRouter);
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/api/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await router.handle(req);
|
||||||
|
assertEquals(middlewareExecuted, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Context State", () => {
|
||||||
|
it("should maintain state across middleware chain", async () => {
|
||||||
|
router.use(async (_, ctx, next) => {
|
||||||
|
ctx.state.test = "value";
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/test")
|
||||||
|
.get(async (_, ctx) => {
|
||||||
|
return new Response(ctx.state.test as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(await res.text(), "value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
it("should handle errors in handlers", async () => {
|
||||||
|
router.route("/error")
|
||||||
|
.get(async () => {
|
||||||
|
throw new Error("Test error");
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/error", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 500);
|
||||||
|
assertEquals(await res.text(), "Internal Server Error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors in middleware", async () => {
|
||||||
|
router.use(async () => {
|
||||||
|
throw new Error("Middleware error");
|
||||||
|
});
|
||||||
|
|
||||||
|
router.route("/test")
|
||||||
|
.get(async () => new Response("test"));
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTTP Methods", () => {
|
||||||
|
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
||||||
|
|
||||||
|
methods.forEach((method) => {
|
||||||
|
it(`should handle ${method} requests`, async () => {
|
||||||
|
const route = router.route("/test");
|
||||||
|
route[method.toLowerCase() as keyof typeof route](
|
||||||
|
async () => new Response(method),
|
||||||
|
);
|
||||||
|
|
||||||
|
const req = new Request("http://localhost/test", {
|
||||||
|
method: method,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await router.handle(req);
|
||||||
|
assertEquals(res.status, 200);
|
||||||
|
assertEquals(await res.text(), method);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
281
router.new.ts
Normal file
281
router.new.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import type { RouterContext as oldContext } from "@bearmetal/router/types";
|
||||||
|
import { joinPaths } from "./util/join.ts";
|
||||||
|
import { InternalError, NotFound } from "@bearmetal/router/util/response";
|
||||||
|
|
||||||
|
type RouterContext = Omit<oldContext, "pattern">;
|
||||||
|
type Handler = (
|
||||||
|
req: Request,
|
||||||
|
ctx: RouterContext,
|
||||||
|
next: () => Promise<Response>,
|
||||||
|
) => 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(handler: Handler): void;
|
||||||
|
get(path: string, handler: Handler): void;
|
||||||
|
get(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(GET, config).push(handler!);
|
||||||
|
}
|
||||||
|
post(handler: Handler): void;
|
||||||
|
post(path: string, handler: Handler): void;
|
||||||
|
post(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(POST, config).push(handler!);
|
||||||
|
}
|
||||||
|
put(handler: Handler): void;
|
||||||
|
put(path: string, handler: Handler): void;
|
||||||
|
put(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(PUT, config).push(handler!);
|
||||||
|
}
|
||||||
|
patch(handler: Handler): void;
|
||||||
|
patch(path: string, handler: Handler): void;
|
||||||
|
patch(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(PATCH, config).push(handler!);
|
||||||
|
}
|
||||||
|
delete(handler: Handler): void;
|
||||||
|
delete(path: string, handler: Handler): void;
|
||||||
|
delete(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(DELETE, config).push(handler!);
|
||||||
|
}
|
||||||
|
options(handler: Handler): void;
|
||||||
|
options(path: string, handler: Handler): void;
|
||||||
|
options(path: string | Handler, handler?: Handler): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
path = fixPath(path);
|
||||||
|
}
|
||||||
|
const config = this.getOrCreateConfig(path);
|
||||||
|
this.getOrCreateConfigHandlers(OPTIONS, config).push(handler!);
|
||||||
|
}
|
||||||
|
|
||||||
|
use(handler: Handler | Router): void;
|
||||||
|
use(path: string, handler: Handler | Router): void;
|
||||||
|
use(path: string | Handler | Router, handler?: Handler | Router): void {
|
||||||
|
if (typeof path !== "string") {
|
||||||
|
handler = path;
|
||||||
|
path = "/.*";
|
||||||
|
} else {
|
||||||
|
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 }),
|
||||||
|
};
|
||||||
|
this.routes.set(path, config);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
if (!matchingRoutes.length) return NotFound();
|
||||||
|
const matchingMethods = matchingRoutes.some((r) =>
|
||||||
|
Object.hasOwn(r.config.handlers, method)
|
||||||
|
);
|
||||||
|
if (!matchingMethods) {
|
||||||
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
const middlewareStack = matchingRoutes.flatMap((r) =>
|
||||||
|
(r.config.handlers[_use] ?? []).concat(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 InternalError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
20
router.test.ts
Executable file → Normal file
20
router.test.ts
Executable file → Normal file
@@ -80,7 +80,7 @@ describe("Router", () => {
|
|||||||
it("should execute global middleware", async () => {
|
it("should execute global middleware", async () => {
|
||||||
let middlewareExecuted = false;
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
router.use(async (_ctx, next) => {
|
router.use(async (_, _ctx, next) => {
|
||||||
middlewareExecuted = true;
|
middlewareExecuted = true;
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
@@ -99,9 +99,9 @@ describe("Router", () => {
|
|||||||
it("should execute path-specific middleware", async () => {
|
it("should execute path-specific middleware", async () => {
|
||||||
let middlewareExecuted = false;
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
router.use("/test", async (_ctx, next) => {
|
router.use("/test", async (_, _ctx, next) => {
|
||||||
middlewareExecuted = true;
|
middlewareExecuted = true;
|
||||||
console.log('middlware happened')
|
console.log("middlware happened");
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ describe("Router", () => {
|
|||||||
it("should handle middleware parameters", async () => {
|
it("should handle middleware parameters", async () => {
|
||||||
const capturedParams: Record<string, string | undefined> = {};
|
const capturedParams: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
router.use("/:version/.*", async (ctx, next) => {
|
router.use("/:version/.*", async (_, ctx, next) => {
|
||||||
capturedParams.version = ctx.params.version;
|
capturedParams.version = ctx.params.version;
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
@@ -138,12 +138,12 @@ describe("Router", () => {
|
|||||||
it("should execute middleware in correct order", async () => {
|
it("should execute middleware in correct order", async () => {
|
||||||
const order: number[] = [];
|
const order: number[] = [];
|
||||||
|
|
||||||
router.use(async (_ctx, next) => {
|
router.use(async (_, _ctx, next) => {
|
||||||
order.push(1);
|
order.push(1);
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use("/test", async (_ctx, next) => {
|
router.use("/test", async (_, _ctx, next) => {
|
||||||
order.push(2);
|
order.push(2);
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
@@ -198,7 +198,7 @@ describe("Router", () => {
|
|||||||
const apiRouter = new Router();
|
const apiRouter = new Router();
|
||||||
let middlewareExecuted = false;
|
let middlewareExecuted = false;
|
||||||
|
|
||||||
apiRouter.use(async (_ctx, next) => {
|
apiRouter.use(async (_, _ctx, next) => {
|
||||||
middlewareExecuted = true;
|
middlewareExecuted = true;
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
@@ -219,7 +219,7 @@ describe("Router", () => {
|
|||||||
|
|
||||||
describe("Context State", () => {
|
describe("Context State", () => {
|
||||||
it("should maintain state across middleware chain", async () => {
|
it("should maintain state across middleware chain", async () => {
|
||||||
router.use(async (ctx, next) => {
|
router.use(async (_, ctx, next) => {
|
||||||
ctx.state.test = "value";
|
ctx.state.test = "value";
|
||||||
return await next();
|
return await next();
|
||||||
});
|
});
|
||||||
@@ -274,11 +274,11 @@ describe("Router", () => {
|
|||||||
describe("HTTP Methods", () => {
|
describe("HTTP Methods", () => {
|
||||||
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
||||||
|
|
||||||
methods.forEach(method => {
|
methods.forEach((method) => {
|
||||||
it(`should handle ${method} requests`, async () => {
|
it(`should handle ${method} requests`, async () => {
|
||||||
const route = router.route("/test");
|
const route = router.route("/test");
|
||||||
route[method.toLowerCase() as keyof typeof route](
|
route[method.toLowerCase() as keyof typeof route](
|
||||||
async () => new Response(method)
|
async () => new Response(method),
|
||||||
);
|
);
|
||||||
|
|
||||||
const req = new Request("http://localhost/test", {
|
const req = new Request("http://localhost/test", {
|
||||||
|
2
router.ts
Executable file → Normal file
2
router.ts
Executable file → Normal file
@@ -393,7 +393,7 @@ export class Router {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const file = await Deno.readFile(normalizedPath);
|
const file = await Deno.readFile(normalizedPath);
|
||||||
const filetype = normalizedPath.split(".")[1];
|
const filetype = normalizedPath.split(".").at(-1);
|
||||||
const contentType = getContentTypeByExtension(filetype);
|
const contentType = getContentTypeByExtension(filetype);
|
||||||
return new Response(file, { headers: { "Content-Type": contentType } });
|
return new Response(file, { headers: { "Content-Type": contentType } });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
2
util/contentType.ts
Executable file → Normal file
2
util/contentType.ts
Executable file → Normal file
@@ -1,4 +1,4 @@
|
|||||||
export function getContentTypeByExtension(extension: string) {
|
export function getContentTypeByExtension(extension?: string) {
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
case "html":
|
case "html":
|
||||||
case "htm":
|
case "htm":
|
||||||
|
0
util/isRelativePath.ts
Executable file → Normal file
0
util/isRelativePath.ts
Executable file → Normal file
3
util/join.ts
Normal file
3
util/join.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const joinPaths = (...args: string[]) =>
|
||||||
|
args.map((a, i, l) => i === l.length - 1 ? a : a.replace(/\/?\.?\*?$/, ""))
|
||||||
|
.join("");
|
0
util/response.ts
Executable file → Normal file
0
util/response.ts
Executable file → Normal file
Reference in New Issue
Block a user