alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
212 lines (211 loc) • 7.06 kB
JavaScript
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