Skip to content

Commit

Permalink
make router implement handler for Deno.serve
Browse files Browse the repository at this point in the history
  • Loading branch information
lenkan committed Sep 2, 2023
1 parent 6878ff3 commit 23313b4
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 75 deletions.
8 changes: 3 additions & 5 deletions example/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { HttpRouter } from "../mod.ts";
import { createRouter } from "../mod.ts";

type User = { name: string };
const users: Record<string, User> = {
"1": { name: "John Doe" },
"2": { name: "Jane Doe" },
};

const listener = Deno.listen({ port: 8080 });

const router = new HttpRouter();
const router = createRouter();

router.get("/users/:id", (_req, match) => {
if (!match.pathname.groups.id) {
Expand Down Expand Up @@ -38,4 +36,4 @@ router.all("*", (_match, _req) => {
return Response.json({ message: "Not Found" }, { status: 404 });
});

await router.serve(listener);
Deno.serve({ port: 8080 }, router);
4 changes: 2 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { HttpMethod, HttpRouteHandler } from "./src/router.ts";
export { HttpRouter } from "./src/router.ts";
export type { HttpRouter, HttpMethod, HttpRouteHandler } from "./src/router.ts";
export { createRouter } from "./src/router.ts";
6 changes: 2 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ const users: Record<string, User> = {
"2": { name: "Jane Doe" },
};

const listener = Deno.listen({ port: 8080 });

const router = new HttpRouter();
const router = createRouter();

router.get("/users/:id", (_req, match) => {
const user = users[match.pathname.groups.id];
Expand All @@ -38,5 +36,5 @@ router.all("*", (_match, _req) => {
return Response.json({ message: "Not Found" }, { status: 404 });
});

await router.serve(listener);
Deno.serve({ port: 8080 }, router);
```
103 changes: 55 additions & 48 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,48 @@
export type HttpRouteHandler = (
request: Request,
match: URLPatternResult,
) => Response | Promise<Response>;
export type HttpRouteHandler = (request: Request, match: URLPatternResult) => Response | Promise<Response>;

type HttpRoute = {
pattern: URLPattern;
handler: HttpRouteHandler;
method: string;
};

export type HttpMethod =
| "get"
| "post"
| "delete"
| "patch"
| "put"
| "options";

export class HttpRouter {
#routes: HttpRoute[] = [];

get = (pattern: URLPatternInput, handler: HttpRouteHandler) =>
this.#use("get", pattern, handler);
post = (pattern: URLPatternInput, handler: HttpRouteHandler) =>
this.#use("post", pattern, handler);
put = (pattern: URLPatternInput, handler: HttpRouteHandler) =>
this.#use("put", pattern, handler);
delete = (pattern: URLPatternInput, handler: HttpRouteHandler) =>
this.#use("delete", pattern, handler);

all = (
export type HttpMethod = "get" | "post" | "delete" | "patch" | "put" | "options" | "head";
export type HttpRouteMethod = (pattern: URLPatternInput, handler: HttpRouteHandler) => HttpRouter;

export interface HttpRouter {
(request: Request): Promise<Response>;
all: (
pattern: URLPatternInput,
handler: Partial<Record<HttpMethod, HttpRouteHandler>> | HttpRouteHandler,
) => {
handler: Partial<Record<HttpMethod, HttpRouteHandler>> | HttpRouteHandler
) => HttpRouter;
get: HttpRouteMethod;
post: HttpRouteMethod;
put: HttpRouteMethod;
delete: HttpRouteMethod;
patch: HttpRouteMethod;
options: HttpRouteMethod;
head: HttpRouteMethod;
serve(listener: Deno.Listener): Promise<void>;
}

export function createRouter(): HttpRouter {
const routes: HttpRoute[] = [];

const all = (pattern: URLPatternInput, handler: Partial<Record<HttpMethod, HttpRouteHandler>> | HttpRouteHandler) => {
if (typeof handler === "object") {
for (const [method, h] of Object.entries(handler)) {
this.#use(method as HttpMethod, pattern, h);
use(method as HttpMethod)(pattern, h);
}
} else {
this.#use("all", pattern, handler);
use("all")(pattern, handler);
}

return instance;
};

#use = (
method: HttpMethod | "all",
pattern: string | URLPatternInput,
handler: HttpRouteHandler,
) => {
const use = (method: HttpMethod | "all") => (pattern: string | URLPatternInput, handler: HttpRouteHandler) => {
if (typeof pattern === "string") {
this.#routes.push({
routes.push({
method,
pattern: new URLPattern({
protocol: "http{s}?",
Expand All @@ -63,16 +57,15 @@ export class HttpRouter {
handler,
});
} else {
this.#routes.push({ method, pattern: new URLPattern(pattern), handler });
routes.push({ method, pattern: new URLPattern(pattern), handler });
}

return instance;
};

async handleRequest(request: Request): Promise<Response> {
for (const route of this.#routes) {
if (
route.method.toUpperCase() === "ALL" ||
route.method.toUpperCase() === request.method.toUpperCase()
) {
async function handleRequest(request: Request): Promise<Response> {
for (const route of routes) {
if (route.method.toUpperCase() === "ALL" || route.method.toUpperCase() === request.method.toUpperCase()) {
const url = new URL(request.url, "http://example.com");
const result = route.pattern.exec(url);

Expand All @@ -86,20 +79,34 @@ export class HttpRouter {
throw new Error("Unhandled request");
}

async handleEvent(event: Deno.RequestEvent) {
const response = await this.handleRequest(event.request);
async function handleEvent(event: Deno.RequestEvent) {
const response = await handleRequest(event.request);
await event.respondWith(response);
}

async handleConnection(conn: Deno.Conn) {
async function handleConnection(conn: Deno.Conn) {
for await (const event of Deno.serveHttp(conn)) {
this.handleEvent(event);
handleEvent(event);
}
}

async serve(listener: Deno.Listener) {
async function serve(listener: Deno.Listener) {
for await (const conn of listener) {
this.handleConnection(conn);
handleConnection(conn);
}
}

const instance = Object.assign(handleRequest, {
get: use("get"),
post: use("post"),
delete: use("delete"),
patch: use("patch"),
put: use("put"),
options: use("options"),
head: use("head"),
all,
serve,
});

return instance;
}
27 changes: 11 additions & 16 deletions src/router_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {
assertEquals,
assertRejects,
} from "https://deno.land/[email protected]/assert/mod.ts";
import { assertEquals, assertRejects } from "https://deno.land/[email protected]/assert/mod.ts";

import { HttpRouter } from "./router.ts";
import { createRouter } from "./router.ts";

const baseurl = "http://example.com";

Expand All @@ -12,46 +9,44 @@ function createRequest(path: string) {
}

Deno.test("get random json body", async () => {
const router = new HttpRouter();
const router = createRouter();
const body = { message: crypto.randomUUID() };
router.get("/", () => Response.json(body));

const response = await router.handleRequest(
new Request(new URL("/", baseurl)),
);
const response = await router(new Request(new URL("/", baseurl)));

assertEquals(await response.json(), body);
});

Deno.test("match path parameter", async () => {
const router = new HttpRouter();
const router = createRouter();

router.get("/", () => new Response("not found"));
router.get("/:id", (_, match) => new Response(match.pathname.groups.id));

const id = crypto.randomUUID();
const response1 = await router.handleRequest(createRequest("/"));
const response2 = await router.handleRequest(createRequest("/" + id));
const response1 = await router(createRequest("/"));
const response2 = await router(createRequest("/" + id));

assertEquals(await response1.text(), "not found");
assertEquals(await response2.text(), id);
});

Deno.test("rejects if no match", async () => {
const router = new HttpRouter();
const router = createRouter();

router.get("/", () => new Response("ok"));

await assertRejects(() => router.handleRequest(createRequest("/abc")));
await assertRejects(() => router(createRequest("/abc")));
});

Deno.test("can use fallback if no match", async () => {
const router = new HttpRouter();
const router = createRouter();

router.get("/", () => new Response("ok"));
router.get("/abcd", () => new Response("ok"));
router.all("*", () => new Response("no match"));

const response = await router.handleRequest(createRequest("/abc"));
const response = await router(createRequest("/abc"));
assertEquals(await response.text(), "no match");
});

0 comments on commit 23313b4

Please sign in to comment.