Skip to content

Commit

Permalink
feat!: move additional router options to object (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
eliassjogreen authored Feb 11, 2023
1 parent 7d6c93d commit 641ed3a
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 127 deletions.
2 changes: 1 addition & 1 deletion examples/hello_world.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { serve } from "https://deno.land/std@0.173.0/http/server.ts";
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { router } from "https://deno.land/x/rutt/mod.ts";

await serve(
Expand Down
4 changes: 2 additions & 2 deletions examples/nested_routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { serve } from "https://deno.land/std@0.173.0/http/server.ts";
import { router } from "../mod.ts";
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { router } from "https://deno.land/x/rutt/mod.ts";

await serve(
router({
Expand Down
9 changes: 9 additions & 0 deletions examples/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { router } from "https://deno.land/x/rutt/mod.ts";

await serve(
router({
"/hello/:name": (_req, _, { name }) =>
new Response(`Hello ${name}`, { status: 200 }),
}),
);
140 changes: 82 additions & 58 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
/**
* Rutt is a tiny http router designed for use with deno and deno deploy.
* It is written in about 200 lines of code and is pretty fast, using an
* It is written in about 300 lines of code and is fast enough, using an
* extended type of the web-standard {@link URLPattern} to provide fast and
* easy route matching.
*
* @example
* ```ts
* import { serve } from "https://deno.land/std/http/server.ts";
* import { router } from "https://deno.land/x/rutt/mod.ts";
*
* await serve(
* router({
* "/": (_req) => new Response("Hello world!", { status: 200 }),
* }),
* );
* ```
*
* @module
*/

import type { ConnInfo } from "https://deno.land/std@0.173.0/http/server.ts";
import type { ConnInfo } from "https://deno.land/std@0.177.0/http/server.ts";

/**
* Provides arbitrary context to {@link Handler} functions along with
Expand Down Expand Up @@ -40,7 +52,7 @@ export type ErrorHandler<T = unknown> = (
export type UnknownMethodHandler<T = unknown> = (
req: Request,
ctx: HandlerContext<T>,
knownMethods: string[],
knownMethods: KnownMethod[],
) => Response | Promise<Response>;

/**
Expand Down Expand Up @@ -88,6 +100,45 @@ export type InternalRoute<T = {}> = {
// deno-lint-ignore ban-types
export type InternalRoutes<T = {}> = InternalRoute<T>[];

/**
* Additional options for the {@link router} function.
*/
export interface RouterOptions<T> {
/**
* An optional property which contains a handler for anything that doesn't
* match the `routes` parameter
*/
otherHandler?: Handler<T>;
/**
* An optional property which contains a handler for any time it fails to run
* the default request handling code
*/
errorHandler?: ErrorHandler<T>;
/**
* An optional property which contains a handler for any time a method that
* is not defined is used
*/
unknownMethodHandler?: UnknownMethodHandler<T>;
}

/**
* A known HTTP method.
*/
export type KnownMethod = typeof knownMethods[number];

/**
* All known HTTP methods.
*/
export const knownMethods = [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"OPTIONS",
"PATCH",
] as const;

/**
* The default other handler for the router. By default it responds with `null`
* body and a status of 404.
Expand Down Expand Up @@ -117,12 +168,12 @@ export function defaultErrorHandler(
/**
* The default unknown method handler for the router. By default it responds
* with `null` body, a status of 405 and the `Accept` header set to all
* {@link METHODS known methods}.
* {@link KnownMethod known methods}.
*/
export function defaultUnknownMethodHandler(
_req: Request,
_ctx: HandlerContext,
knownMethods: string[],
knownMethods: KnownMethod[],
): Response {
return new Response(null, {
status: 405,
Expand All @@ -132,20 +183,7 @@ export function defaultUnknownMethodHandler(
});
}

/**
* All known HTTP methods.
*/
export const METHODS = [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"OPTIONS",
"PATCH",
] as const;

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

function joinPaths(a: string, b: string): string {
if (a.endsWith("/")) {
Expand All @@ -169,12 +207,9 @@ export function buildInternalRoutes<T = unknown>(
routes: Routes<T>,
basePath = "/",
): InternalRoutes<T> {
const internalRoutesRecord: Record<
string,
InternalRoute<T>
> = {};
const internalRoutesRecord: Record<string, InternalRoute<T>> = {};
for (const [route, handler] of Object.entries(routes)) {
let [methodOrPath, path] = route.split(methodRegex);
let [methodOrPath, path] = route.split(knownMethodRegex);
let method = methodOrPath;
if (!path) {
path = methodOrPath;
Expand Down Expand Up @@ -203,7 +238,9 @@ export function buildInternalRoutes<T = unknown>(
}

/**
* A simple and tiny router for deno
* A simple and tiny router for deno. This function provides a way of
* constructing a HTTP request handler for the provided {@link routes} and any
* provided {@link RouterOptions}.
*
* @example
* ```ts
Expand All @@ -218,37 +255,32 @@ export function buildInternalRoutes<T = unknown>(
* ```
*
* @param routes A record of all routes and their corresponding handler functions
* @param other An optional parameter which contains a handler for anything that
* doesn't match the `routes` parameter
* @param error An optional parameter which contains a handler for any time it
* fails to run the default request handling code
* @param unknownMethod An optional parameter which contains a handler for any
* time a method that is not defined is used
* @param options An object containing all of the possible configuration options
* @returns A deno std compatible request handler
*/
export function router<T = unknown>(
routes: Routes<T> | InternalRoutes<T>,
other: Handler<T> = defaultOtherHandler,
error: ErrorHandler<T> = defaultErrorHandler,
unknownMethod: UnknownMethodHandler<T> = defaultUnknownMethodHandler,
{ otherHandler, errorHandler, unknownMethodHandler }: RouterOptions<T> = {
otherHandler: defaultOtherHandler,
errorHandler: defaultErrorHandler,
unknownMethodHandler: defaultUnknownMethodHandler,
},
): Handler<T> {
otherHandler ??= defaultOtherHandler;
errorHandler ??= defaultErrorHandler;
unknownMethodHandler ??= defaultUnknownMethodHandler;

const internalRoutes = Array.isArray(routes)
? routes
: buildInternalRoutes(routes);

return async (req, ctx) => {
try {
for (const { pattern, methods } of internalRoutes) {
let res: URLPatternResult | RegExpExecArray | null;
let groups: Record<string, string>;

if (pattern instanceof URLPattern) {
res = pattern.exec(req.url);
groups = res?.pathname.groups ?? {};
} else {
res = pattern.exec(req.url);
groups = res?.groups ?? {};
}
const res = pattern.exec(req.url);
const groups = (pattern instanceof URLPattern
? (res as URLPatternResult | null)?.pathname.groups
: (res as RegExpExecArray | null)?.groups) ?? {};

for (const key in groups) {
groups[key] = decodeURIComponent(groups[key]);
Expand All @@ -257,33 +289,25 @@ export function router<T = unknown>(
if (res !== null) {
for (const [method, handler] of Object.entries(methods)) {
if (req.method === method) {
return await handler(
req,
ctx,
groups,
);
return await handler(req, ctx, groups);
}
}

if (methods["any"]) {
return await methods["any"](
req,
ctx,
groups,
);
return await methods["any"](req, ctx, groups);
} else {
return await unknownMethod(
return await unknownMethodHandler!(
req,
ctx,
Object.keys(methods),
Object.keys(methods) as KnownMethod[],
);
}
}
}

return await other(req, ctx);
return await otherHandler!(req, ctx);
} catch (err) {
return error(req, ctx, err);
return errorHandler!(req, ctx, err);
}
};
}
Loading

0 comments on commit 641ed3a

Please sign in to comment.