Skip to content

Commit

Permalink
feat: nested routes (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
eliassjogreen authored Dec 18, 2022
1 parent 918358e commit e6b6fb9
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 8 deletions.
10 changes: 10 additions & 0 deletions examples/nested_routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { serve } from "https://deno.land/std@0.152.0/http/server.ts";
import { router } from "../mod.ts";

await serve(
router({
"/hello": {
"/world": (_req) => new Response("Hello world!", { status: 200 }),
},
}),
);
42 changes: 34 additions & 8 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export type MatchHandler<T = unknown> = (
* example a route only accepting `GET` requests would look like: `GET@/`.
*/
// deno-lint-ignore ban-types
export type Routes<T = {}> = Record<string, MatchHandler<T>>;
export interface Routes<T = {}> {
[key: string]: Routes<T> | MatchHandler<T>;
}

/**
* The internal route object contains either a {@link RegExp} pattern or
Expand Down Expand Up @@ -145,6 +147,18 @@ export const METHODS = [

const methodRegex = new RegExp(`(?<=^(?:${METHODS.join("|")}))@`);

function joinPaths(a: string, b: string): string {
if (a.endsWith("/")) {
a = a.slice(0, -1);
}

if (!b.startsWith("/") && !b.startsWith("{/}?")) {
b = "/" + b;
}

return a + b;
}

/**
* Builds an {@link InternalRoutes} array from a {@link Routes} record.
*
Expand All @@ -153,10 +167,11 @@ const methodRegex = new RegExp(`(?<=^(?:${METHODS.join("|")}))@`);
*/
export function buildInternalRoutes<T = unknown>(
routes: Routes<T>,
basePath = "/",
): InternalRoutes<T> {
const internalRoutesRecord: Record<
string,
{ pattern: URLPattern; methods: Record<string, MatchHandler<T>> }
InternalRoute<T>
> = {};
for (const [route, handler] of Object.entries(routes)) {
let [methodOrPath, path] = route.split(methodRegex);
Expand All @@ -165,12 +180,23 @@ export function buildInternalRoutes<T = unknown>(
path = methodOrPath;
method = "any";
}
const r = internalRoutesRecord[path] ?? {
pattern: new URLPattern({ pathname: path }),
methods: {},
};
r.methods[method] = handler;
internalRoutesRecord[path] = r;

path = joinPaths(basePath, path);

if (typeof handler === "function") {
const r = internalRoutesRecord[path] ?? {
pattern: new URLPattern({ pathname: path }),
methods: {},
};
r.methods[method] = handler;
internalRoutesRecord[path] = r;
} else {
const subroutes = buildInternalRoutes(handler, path);
for (const subroute of subroutes) {
internalRoutesRecord[(subroute.pattern as URLPattern).pathname] ??=
subroute;
}
}
}

return Object.values(internalRoutesRecord);
Expand Down
218 changes: 218 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,221 @@ Deno.test("handlers", async ({ step }) => {
});
});
});

Deno.test("nesting", async ({ step }) => {
await step("slash", async () => {
const route = router({
"/": () => new Response(),
"/test/": {
"/abc": () => new Response(),
"/123": () => new Response(),
},
});
let response: Response;

response = await route(
new Request("https://example.com/"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/test/abc"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test/123"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});

await step("no slash", async () => {
const route = router({
"": () => new Response(),
"test": {
"abc": () => new Response(),
"123": () => new Response(),
},
});
let response: Response;

response = await route(
new Request("https://example.com/"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/test/abc"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test/123"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});

await step("parameters", async () => {
const route = router({
":test": {
"abc": () => new Response(),
"123": () => new Response(),
},
});
let response: Response;

response = await route(
new Request("https://example.com/foo"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/bar/abc"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/baz/123"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});

await step("nested parent handler", async () => {
const route = router({
"/test": {
"/abc": () => new Response(),
"{/}?": () => new Response(),
},
});
let response: Response;

response = await route(
new Request("https://example.com/test/123"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/test/abc"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test/"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);

response = await route(
new Request("https://example.com/test"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});
});

Deno.test("internal routes", async ({ step }) => {
await step("RegExp", async () => {
const route = router([
{
pattern: /^https:\/\/example\.com\/test$/,
methods: { "any": () => new Response() },
},
]);
let response: Response;

response = await route(
new Request("https://example.com/"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/test"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});

await step("URLPattern", async () => {
const route = router([
{
pattern: new URLPattern({ pathname: "/test" }),
methods: { "any": () => new Response() },
},
]);
let response: Response;

response = await route(
new Request("https://example.com/"),
TEST_CONN_INFO,
);
assert(!response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 404);

response = await route(
new Request("https://example.com/test"),
TEST_CONN_INFO,
);
assert(response.ok);
assertEquals(response.body, null);
assertEquals(response.status, 200);
});
});

0 comments on commit e6b6fb9

Please sign in to comment.