import { Context } from "./context.ts";import { Status } from "./deps.ts";import httpError from "./httpError.ts";import { Middleware, compose } from "./middleware.ts";import { Key, pathToRegExp } from "./pathToRegExp.ts";import { HTTPMethods } from "./types.ts";import { decodeComponent } from "./util.ts";
const { MethodNotAllowed, NotImplemented } = httpError;
interface AllowedMethodsOptions { methodNotAllowed?: () => any;
notImplemented?: () => any; throw?: boolean;}
export interface RouteParams { [key: string]: string | undefined; [key: number]: string | undefined;}
export interface RouterContext< P extends RouteParams = RouteParams, S extends object = { [key: string]: any }> extends Context<S> { params: P;
router: Router;}
export interface RouterMiddleware< P extends RouteParams = RouteParams, S extends object = { [key: string]: any }> { (context: RouterContext<P, S>, next: () => Promise<void>): Promise< void > | void;}
export interface RouterOptions { prefix?: string;
methods?: HTTPMethods[];
sensitive?: boolean;
strict?: boolean;}
interface LayerOptions { ignoreCaptures?: boolean; name?: string; sensitive?: boolean; strict?: boolean;}
class Layer { name: string | null; paramNames: Key[] = []; regexp: RegExp; stack: RouterMiddleware[];
constructor( public path: string, public methods: HTTPMethods[], middleware: RouterMiddleware | RouterMiddleware[], public options: LayerOptions = {} ) { this.name = options.name || null; this.stack = Array.isArray(middleware) ? middleware : [middleware]; if (this.methods.includes("GET")) { this.methods.unshift("HEAD"); } this.regexp = pathToRegExp(path, this.paramNames, options); }
matches(path: string): boolean { return this.regexp.test(path); }
params(captures: string[], existingParams: RouteParams = {}): RouteParams { const params = existingParams; for (let i = 0; i < captures.length; i++) { if (this.paramNames[i]) { const capture = captures[i]; params[this.paramNames[i].name] = capture ? decodeComponent(capture) : capture; } } return params; }
captures(path: string): string[] { if (this.options.ignoreCaptures) { return []; } const [, ...captures] = path.match(this.regexp)!; return captures; }
setPrefix(prefix: string): this { if (this.path) { this.path = `${prefix}${this.path}`; this.paramNames = []; this.regexp = pathToRegExp(this.path, this.paramNames, this.options); } return this; }}
const contextRouteMatches = new WeakMap<RouterContext, Layer[]>();
export class Router { private _methods: HTTPMethods[]; private _stack: Layer[] = []; private _prefix = "";
private _addRoute( path: string | string[], middleware: RouterMiddleware[], ...methods: HTTPMethods[] ): this { if (Array.isArray(path)) { for (const r of path) { this._addRoute(r, middleware, ...methods); } return this; } const layer = new Layer(path, methods, middleware); layer.setPrefix(this._prefix); this._stack.push(layer); return this; }
private _match( path: string, method: HTTPMethods ): { routesMatched: Layer[]; matches: Layer[] } { const routesMatched: Layer[] = []; const matches: Layer[] = []; for (const layer of this._stack) { if (layer.matches(path)) { routesMatched.push(layer); if (layer.methods.includes(method)) { matches.push(layer); } } } return { routesMatched, matches }; }
constructor(options: RouterOptions = {}) { this._methods = options.methods || [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT" ]; if (options.prefix) this._prefix = options.prefix; }
all<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute( route, middleware as RouterMiddleware[], "DELETE", "GET", "POST", "PUT" ); }
allowedMethods(options: AllowedMethodsOptions = {}): Middleware { const implemented = this._methods; return async function allowedMethods(context, next) { await next(); const allowed = new Set<HTTPMethods>(); if ( !context.response.status || context.response.status === Status.NotFound ) { const contextRoutesMatched = contextRouteMatches.get( context as RouterContext ); if (contextRoutesMatched) { for (const layer of contextRoutesMatched) { for (const method of layer.methods) { allowed.add(method); } } } const allowedValue = Array.from(allowed).join(", "); if (!implemented.includes(context.request.method)) { if (options.throw) { let notImplementedThrowable: any; if (typeof options.notImplemented === "function") { notImplementedThrowable = options.notImplemented(); } else { notImplementedThrowable = new NotImplemented(); } throw notImplementedThrowable; } else { context.response.status = Status.NotImplemented; context.response.headers.set("Allow", allowedValue); } } else if (allowed.size) { if (context.request.method === "OPTIONS") { context.response.status = Status.OK; context.response.body = ""; context.response.headers.set("Allow", allowedValue); } else if (!allowed.has(context.request.method)) { if (options.throw) { let notAllowedThrowable: any; if (typeof options.methodNotAllowed === "function") { notAllowedThrowable = options.methodNotAllowed(); } else { notAllowedThrowable = new MethodNotAllowed(); } throw notAllowedThrowable; } else { context.response.status = Status.MethodNotAllowed; context.response.headers.set("Allow", allowedValue); } } } } }; }
delete<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "DELETE"); }
get<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "GET"); }
head<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "HEAD"); }
options<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "OPTIONS"); }
patch<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "PATCH"); }
post<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "POST"); }
put<P extends RouteParams = RouteParams>( route: string | string[], ...middleware: RouterMiddleware<P>[] ): this { return this._addRoute(route, middleware as RouterMiddleware[], "PUT"); }
routes(): Middleware { const dispatch = async ( context: RouterContext, next: () => Promise<void> ): Promise<void> => { const { path, method } = context.request; const { routesMatched, matches } = this._match(path, method);
const contextRoutesMatched = contextRouteMatches.get(context); contextRouteMatches.set( context, contextRoutesMatched ? [...contextRoutesMatched, ...routesMatched] : routesMatched );
context.router = this;
if (!matches.length) { return next(); }
const chain = matches.reduce( (prev, layer) => { prev.push((context: RouterContext, next: () => Promise<void>) => { const captures = layer.captures(path); context.params = layer.params(captures, context.params); return next(); }); return [...prev, ...layer.stack]; }, [] as RouterMiddleware[] ); return compose(chain)(context as RouterContext); }; return dispatch as Middleware; }}