UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

212 lines (211 loc) 7.06 kB
import { AlephaError } from "alepha"; //#region ../../src/router/providers/RouterProvider.ts var RouterProvider = class { routePathRegex = /^\/[A-Za-z0-9._~!$&%'()*+,;=:@{}?/-]*$/; tree = { children: {} }; cache = /* @__PURE__ */ new Map(); maxCacheSize = 1e4; match(path) { const pathname = path.split("?", 1)[0]; const hit = this.cache.get(pathname); if (hit) return { route: hit.route, params: { ...hit.params } }; const result = this.mapParams(this.createRouteMatch(pathname)); if (this.cache.size >= this.maxCacheSize) this.cache.clear(); this.cache.set(pathname, result); return { route: result.route, params: { ...result.params } }; } test(path) { if (!this.routePathRegex.test(path)) throw new AlephaError(`Route '${path}' is not valid`); } push(route) { const path = route.path.replaceAll("//", "/"); this.test(path); this.cache.clear(); const parts = this.createParts(path); let cursor = this.tree; for (let i = 0; i < parts.length; i++) { const isLast = i === parts.length - 1; let part = parts[i].toLowerCase(); if (part === "*" && isLast) { cursor.wildcard = { route }; break; } if (part.includes("*")) throw new AlephaError(`Route '${path}' has an invalid wildcard syntax`); if (part.includes("{") || part.includes("}")) if (part.startsWith("{") && part.endsWith("}")) part = `:${part.slice(1, -1)}`; else throw new AlephaError(`Route '${path}' has an invalid param syntax`); if (part.startsWith(":")) { const name = parts[i].slice(1).replaceAll("}", ""); if (!name) throw new AlephaError(`Route '${path}' has an empty param name`); if (!cursor.param) cursor.param = { name, children: {} }; else if (cursor.param.name !== name) { route.mapParams ??= {}; route.mapParams[cursor.param.name] = name; } if (isLast) cursor.param.route = route; cursor = cursor.param; continue; } if (!cursor.children[part]) cursor.children[part] = { children: {} }; if (isLast) cursor.children[part].route = route; cursor = cursor.children[part]; } } createRouteMatch(path) { if (path[0] !== "/") throw new AlephaError(`Path '${path}' must start with "/"`); const parts = this.createParts(path); let cursor = this.tree; let wildcard; const params = {}; for (let i = 0; i < parts.length; i++) { const part = parts[i].toLowerCase(); if (cursor.children[part]) { if (cursor.wildcard) wildcard = cursor.wildcard; cursor = cursor.children[part]; } else if (cursor.param) { if (cursor.wildcard) wildcard = cursor.wildcard; params[cursor.param.name] = parts[i]; cursor = cursor.param; } else if (cursor.wildcard) { params["*"] = parts.slice(i).join("/"); return { route: cursor.wildcard.route, params }; } else return { route: wildcard?.route, params }; } if (!cursor?.route) { if (cursor.wildcard) return { route: cursor.wildcard.route, params }; return { route: wildcard?.route, params }; } return { route: cursor.route, params }; } mapParams(match) { if (match.route?.mapParams && match.params) { for (const [key, value] of Object.entries(match.route.mapParams)) if (match.params[key]) { match.params[value] = match.params[key]; delete match.params[key]; } } return match; } createParts(path) { let pathname = path.split("?")[0].replaceAll("//", "/"); if (pathname.endsWith("/") && pathname.length > 1) pathname = pathname.slice(0, -1); return pathname.split("/").slice(1); } }; //#endregion //#region ../../src/router/TemplatedPathParser.ts /** * Parses and manipulates templated paths with `{param}` placeholders. * * Used by both RouterProvider (HTTP routes) and TopicProvider (pub/sub topics) * to handle parameterized path templates in a unified way. * * @example * ```ts * const parser = new TemplatedPathParser("/users/{userId}/posts/{postId}"); * parser.interpolate({ userId: "7", postId: "42" }); // "/users/7/posts/42" * parser.extract("/users/7/posts/42"); // { userId: "7", postId: "42" } * parser.wildcardize("+"); // "/users/+/posts/+" * ``` * * @example * ```ts * // Redis-style colon-separated keys * const parser = new TemplatedPathParser("cache:{namespace}:{key}", ":"); * parser.interpolate({ namespace: "users", key: "42" }); // "cache:users:42" * parser.wildcardize("*"); // "cache:*:*" * ``` */ var TemplatedPathParser = class TemplatedPathParser { static PARAM_REGEX = /\{([^}]+)\}/g; template; separator; paramNames; hasParams; extractRegex; constructor(template, separator = "/") { if (separator.length !== 1) throw new AlephaError(`TemplatedPathParser separator must be a single character, got '${separator}'`); this.template = template; this.separator = separator; this.paramNames = [...template.matchAll(TemplatedPathParser.PARAM_REGEX)].map((m) => m[1]); this.hasParams = this.paramNames.length > 0; this.extractRegex = this.hasParams ? this.buildExtractRegex() : null; } /** * Replaces each `{param}` in the template with the corresponding value * from the provided params record. */ interpolate(params) { return this.template.replace(TemplatedPathParser.PARAM_REGEX, (_, name) => params[name] ?? `{${name}}`); } /** * Extracts parameter values from a concrete path by matching it against * the template structure. * * Returns `null` when the path does not match the template. * Returns `{}` when the template has no parameters and the path matches. */ extract(path) { if (!this.extractRegex) return path === this.template ? {} : null; const match = this.extractRegex.exec(path); if (!match) return null; const result = {}; for (let i = 0; i < this.paramNames.length; i++) result[this.paramNames[i]] = match[i + 1]; return result; } /** * Replaces each `{param}` placeholder in the template with the given * wildcard string. Defaults to `"+"` (MQTT-style). */ wildcardize(wildcard = "+") { return this.template.replace(TemplatedPathParser.PARAM_REGEX, wildcard); } /** * Normalises a path by collapsing repeated separators and stripping a * trailing separator (unless the path is just the separator itself). */ normalize(path) { const sep = this.separator; const escapedSep = this.escapeRegex(sep); let result = path.replace(new RegExp(`${escapedSep}{2,}`, "g"), sep); if (result.endsWith(sep) && result.length > sep.length) result = result.slice(0, -sep.length); return result; } buildExtractRegex() { const escapedSeparator = this.escapeRegex(this.separator); const regexSource = this.template.replace(/[.*+?^${}()|[\]\\]/g, (char) => { if (char === "{" || char === "}") return char; return `\\${char}`; }).replace(/\{[^}]+\}/g, `([^${escapedSeparator}]+)`); return new RegExp(`^${regexSource}$`); } escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } }; //#endregion export { RouterProvider, TemplatedPathParser }; //# sourceMappingURL=index.js.map