Skip to content

Commit

Permalink
feat(router): add basePath option that change router base path
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Sep 11, 2022
1 parent 3cc15e0 commit 329c2bf
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 26 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
createRouter({}, { withHead: false });
```

## Handle base path

Change the router base path.

Just as you could use baseURL or base tags on the Web, you can change the
`basePath` of your router.

```ts
import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";
const api = createRouter({
"/hello": () => new Response("world"),
}, { basePath: "/api" });

const res = await api(new Request("http://localhost/api/hello"));
assertEquals(res.ok, true);
```

The `basePath` and route path are merged without overlapping slashes.

## API

All APIs can be found in the
Expand Down
4 changes: 3 additions & 1 deletion deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
export {
isEmpty,
isFunction,
isString,
} from "https://deno.land/x/isx@1.0.0-beta.19/mod.ts";
export {
Status,
STATUS_TEXT,
} from "https://deno.land/std@0.154.0/http/http_status.ts";
} from "https://deno.land/std@0.155.0/http/http_status.ts";
export { join } from "https://deno.land/std@0.155.0/path/mod.ts";
81 changes: 56 additions & 25 deletions router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// Copyright 2022-latest the httpland authors. All rights reserved. MIT license.
// This module is browser compatible.

import { isEmpty, isFunction, Status, STATUS_TEXT } from "./deps.ts";
import {
isEmpty,
isFunction,
isString,
join,
Status,
STATUS_TEXT,
} from "./deps.ts";

/** HTTP request method. */
export type Method =
Expand Down Expand Up @@ -70,6 +77,21 @@ export interface Options {
* @default true
*/
withHead?: boolean;

/** Change the router base path.
* The `basePath` and route path are merged without overlapping slashes.
* ```ts
* import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
* import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts";
* const api = createRouter({
* "/hello": () => new Response("world"),
* }, { basePath: "/api" });
*
* const res = await api(new Request("http://localhost/api/hello"));
* assertEquals(res.ok, true);
* ```
*/
basePath?: string;
}

/** Map for HTTP method and {@link RouteHandler} */
Expand Down Expand Up @@ -117,30 +139,15 @@ function methods(
*/
export function createRouter(
routes: Routes,
{ withHead = true }: Options = {},
{ withHead = true, basePath }: Options = {},
): Router {
const routeMap = createRouteMap(routes, { withHead });

return (req) => resolveRequest(routeMap, req);
}

type RouteMap = Map<URLPattern, RouteHandler>;

function createRouteMap(
routes: Routes,
options: Options,
): RouteMap {
const entries = Object.entries(routes).filter(isValidRouteEntry).map(
([route, handlerLike]) => {
const handler = resolveHandlerLike(handlerLike, options);
createResolvedHandlerEntry(withHead),
).map(createUrlPatternHandlerEntry(basePath));

return [route, handler] as const;
},
).map(([route, handler]) => {
return [new URLPattern({ pathname: route }), handler] as const;
});
const routeMap = new Map<URLPattern, RouteHandler>(entries);

return new Map<URLPattern, RouteHandler>(entries);
return (req) => resolveRequest(routeMap, req);
}

type RouteEntry = readonly [
Expand All @@ -156,18 +163,42 @@ function isValidRouteEntry(

function resolveHandlerLike(
handlerLike: RouteHandler | MethodRouteHandlers,
options: Options,
withHead: Options["withHead"],
): RouteHandler {
if (isFunction(handlerLike)) {
return handlerLike;
}
const methodHandler = options.withHead
? withHeadHandler(handlerLike)
: handlerLike;
const methodHandler = withHead ? withHeadHandler(handlerLike) : handlerLike;

return methods(methodHandler);
}

function createResolvedHandlerEntry(withHead: Options["withHead"]) {
function createEntry(
[route, handlerLike]: RouteEntry,
): [route: string, handler: RouteHandler] {
const handler = resolveHandlerLike(handlerLike, withHead);

return [route, handler];
}

return createEntry;
}

function createUrlPatternHandlerEntry(basePath: Options["basePath"]) {
function createEntry(
[route, handler]: [route: string, handler: RouteHandler],
): [pattern: URLPattern, handler: RouteHandler] {
const pathname = isString(basePath) ? join(basePath, route) : route;
return [
new URLPattern({ pathname }),
handler,
];
}

return createEntry;
}

async function resolveRequest(
routeMap: Iterable<[pattern: URLPattern, routeHandler: RouteHandler]>,
req: Request,
Expand Down
38 changes: 38 additions & 0 deletions router_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,41 @@ it(
);
},
);

it(
describeTests,
`should return 200 when basePath is joined`,
async () => {
const router = createRouter({
"/hello": () => new Response(null),
}, {
basePath: "/api",
});
const res = await router(
new Request("http://localhost/api/hello"),
);

expect(res).toEqualResponse(
new Response(null, { status: Status.OK }),
);
},
);

it(
describeTests,
`should return 200 when basePath is dirty`,
async () => {
const router = createRouter({
"/hello": () => new Response(null),
}, {
basePath: "/api///",
});
const res = await router(
new Request("http://localhost/api/hello"),
);

expect(res).toEqualResponse(
new Response(null, { status: Status.OK }),
);
},
);

0 comments on commit 329c2bf

Please sign in to comment.