import { createLogger } from '@/log'; interface Route { regex: RegExp; path: string; value: T; } interface Match { value: T; params: Record; regex: RegExp; } const log = createLogger(); const pathToRegex = (path: string): RegExp => { const substitutions: Array<[RegExp, string]> = [ [/\/:(\w+)\?/g, '(?:/(?<$1>[\\w-%.=]+))?'], [/:(\w+)/g, '(?<$1>[\\w-%.=]+)'], [/\*/g, '(?.*?)'], [/\/*/, '/'], [/\/$/, '/?'], ]; const pattern = substitutions.reduce( (acc, [searchValue, replaceValue]) => acc.replace(searchValue, replaceValue), path, ); return new RegExp(`^${pattern}/?$`); }; export class Router { routes: Array>; constructor() { this.routes = []; } use(path: string, ...values: Array>): void { const regex = pathToRegex(path); values.forEach((value) => { if (value instanceof Router) { log.debug(`nested ${path}`); value.routes.forEach((route) => { this.use(path + route.path, route.value); }); } else { log.debug(`registering ${path}`); this.routes.push({ path: path.replaceAll(/\/+/g, '/'), regex, value, }); } }); } *match(path: string): Generator> { log.debug(`matching ${path}`); for (const { regex, value } of this.routes) { const exec = regex.exec(path); if (exec !== null) { log.debug(`matched ${regex}`); const match: Match = { params: exec.groups || {}, regex, value, }; yield match; } } } }