Skip to content

Commit

Permalink
feat(router): add detect routing table error
Browse files Browse the repository at this point in the history
close #6
  • Loading branch information
TomokiMiyauci committed Sep 13, 2022
1 parent d2cc85b commit 0f70875
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 9 deletions.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,89 @@ The route handler receives the following context.
| route | `string`<br> Route pathname. |
| pattern | `URLPattern`<br>URL pattern. |

## Nested route

Nested route is supported.

The nested root is a flat route syntax sugar. Nesting can be as deep as desired.

```ts
import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
createRouter({
"/api": {
"status": () => new Response("OK"),
"hello": {
GET: () => new Response("world!"),
},
},
});
```

This matches the following pattern:

- /api/status
- [GET] /api/hello
- [HEAD] /api/hello (if [withHead](#head-request-handler) is not `false`)

### Joining path segment

Path segments are joined without overlapping slashes.

The result is the same with or without slashes between path segments.

```ts
import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
createRouter({
"/api": {
"status": () => new Response("OK"),
"/status": () => new Response("OK"),
},
"/api/status": () => new Response("OK"),
});
```

They all represent the same URL pattern.

## Throwing error

Routers may throw an error during initialization.

If an error is detected in the user-defined routing table, an error is thrown.

Error in the routing table:

- Duplicate route
- Duplicate route and HTTP method pairs

These prevent you from writing multiple routing tables with the same meaning and
protect you from unexpected bugs.

Throwing error patterns:

```ts
import { createRouter } from "https://deno.land/x/http_router@$VERSION/mod.ts";
createRouter({
"/api": {
"status": () => new Response("OK"),
"/status": () => new Response("OK"),
},
"/api/status": () => new Response("OK"),
}); // duplicate /api/status
createRouter({
"/api": {
"status": {
GET: () => new Response("OK"),
},
},
"/api/status": {
GET: () => new Response("OK"),
},
}); // duplicate [GET] /api/status
```

router detects as many errors as possible and throws errors. In this case, it
throws `AggregateError`, which has `RouterError` as a child.

## URL match pattern

URL patterns can be defined using the
Expand Down
25 changes: 25 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,33 @@ export {
Status,
STATUS_TEXT,
} from "https://deno.land/std@0.155.0/http/http_status.ts";
export {
partition,
} from "https://deno.land/std@0.155.0/collections/partition.ts";
export { mapValues } from "https://deno.land/std@0.155.0/collections/map_values.ts";
export { groupBy } from "https://deno.land/std@0.155.0/collections/group_by.ts";

export function isEmptyObject(value: unknown): value is Record<never, never> {
return !Object.getOwnPropertyNames(value).length &&
!Object.getOwnPropertySymbols(value).length;
}

export function duplicateBy<T>(
value: Iterable<T>,
selector: (el: T, prev: T) => boolean,
): T[] {
const selectedValues: T[] = [];
const ret: T[] = [];

for (const element of value) {
const has = selectedValues.some((v) => selector(element, v));

if (has) {
ret.push(element);
} else {
selectedValues.push(element);
}
}

return ret;
}
51 changes: 42 additions & 9 deletions router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This module is browser compatible.

import {
duplicateBy,
groupBy,
isFunction,
isString,
Expand Down Expand Up @@ -119,8 +120,8 @@ function methods(
* });
* await serve(router);
* ```
* @throws TypeError
* - The given route path is invalid url path.
* @throws AggregateError
* - If the routing table is invalid.
*/
export function createRouter(
routes: Routes,
Expand All @@ -130,7 +131,10 @@ export function createRouter(
const result = groupRouteInfo(routeInfos);

if (!result.valid) {
throw new AggregateError(result.errors);
throw new AggregateError(
result.errors,
`One or more errors were detected in the routing table.`,
);
}

const entries = Object.entries(result.data).map(
Expand Down Expand Up @@ -198,18 +202,47 @@ export function groupRouteInfo(
({ method }) => !!method,
);

const routeGroup = groupBy(withMethodHandlers, ({ route }) => route);
const groupedRouteInfo = groupBy(rawHandlers, ({ route }) => route);
const duplicatedWithRoute = duplicateBy(
rawHandlers,
({ route }, prev) => route === prev.route,
);
const duplicatedWithMethodAndRoute = duplicateBy(
withMethodHandlers,
({ method, route }, prev) => route === prev.route && method === prev.method,
);

const duplicated = duplicatedWithRoute.concat(duplicatedWithMethodAndRoute);

if (duplicated.length) {
const errors = duplicated.map(({ method, route }) => {
const methodStr = method ? `[${method}] ` : "";
return new RouterError(`Duplicated routes. ${methodStr}${route}`);
}) as [RouterError, ...RouterError[]];

return {
valid: false,
errors,
};
}

const routeGroup = groupBy(
withMethodHandlers,
({ route }) => route,
) as Record<string, RouteInfo[]>;
const groupedRouteInfo = groupBy(rawHandlers, ({ route }) => route) as Record<
string,
RouteInfo[]
>;

const routeHandlers = mapValues(
const routeHandlers: Record<string, RouteHandler> = mapValues(
groupedRouteInfo,
(value) => value!.reduce((_, cur) => cur).handler,
(value) => value.reduce((_, cur) => cur).handler,
);

const methodRouteHandlers = mapValues(
const methodRouteHandlers: Record<string, MethodRouteHandlers> = mapValues(
routeGroup,
(routeInfos) =>
routeInfos!.reduce((acc, cur) => {
routeInfos.reduce((acc, cur) => {
if (cur.method) {
acc[cur.method] = cur.handler;
}
Expand Down
55 changes: 55 additions & 0 deletions router_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,3 +648,58 @@ it(
);
},
);

it(
describeTests,
`should throw when the route is deprecated`,
() => {
expect(() =>
createRouter({
"/": {
"/api": () => new Response(),
},
"/api": () => new Response(),
})
).toThrow(`One or more errors were detected in the routing table.`);
},
);

it(
describeTests,
`should throw when the route and method is deprecated`,
() => {
expect(() =>
createRouter({
"/": {
"/api": {
GET: () => new Response(),
},
},
"/api": {
GET: () => new Response(),
},
})
).toThrow();
},
);

it(
describeTests,
`should throw multiple error when the route and method is deprecated`,
() => {
expect(() =>
createRouter({
"/": {
"/api": {
GET: () => new Response(),
POST: () => new Response(),
},
},
"/api": {
GET: () => new Response(),
POST: () => new Response(),
},
})
).toThrow();
},
);

0 comments on commit 0f70875

Please sign in to comment.