"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UrlRouter = exports.UrlRule = void 0; const optional_1 = require("../types/optional"); const urls = require("../util/urls"); // endregion // region private URLRule helpers. /** * Checks whether or not a given pathComponents component contains a parameter. * @param pathComponent - The pathComponents component to check. * @returns true if the pathComponents component is surrounded by curly braces; false otherwise. */ function isPathComponentParameter(pathComponent) { return pathComponent.startsWith("{") && pathComponent.endsWith("}"); } /** * Extracts the parameter contained in a pathComponents component. * @param pathComponent - A pathComponents component surrounded by curly braces. * @returns The parameter contained in the component. */ function getPathComponentParameter(pathComponent) { return pathComponent.replace("{", "").replace("}", ""); } /** * Creates a mapping from key to pathComponents component index * for efficiently extracting parameters from a pathComponents. * @param rulePath - The pathComponents to create a mapping for. * @returns A map of keys to pathComponents component indexes. */ function makePathParameterMapping(rulePath) { const mapping = {}; rulePath.forEach((ruleComponent, index) => { if (isPathComponentParameter(ruleComponent)) { mapping[ruleComponent] = index; } }); return mapping; } /** * Creates `UrlRouteQuery` objects from substring of url. * @param parameters - strings of form `[?]=`. * @returns Array of `UrlRouteQuery` objects. */ function parseQuery(parameters) { const parsedQuery = []; if ((0, optional_1.isNothing)(parameters)) { return parsedQuery; } for (const param of parameters) { const parts = param.split("="); let key = parts[0]; const optional = key.includes("?"); key = key.replace("?", ""); let value = null; if (parts.length > 1) { value = decodeURIComponent(parts[1]); } parsedQuery.push({ key, value, optional, }); } return parsedQuery; } /** * The `UrlRule` class extracts the pattern format from `UrlRuleDefinition`s, and encapsulates * the information needed to match against a candidate URL and extract parameters from it. * * The terminology here is: * - rule: A specific url pattern. * - route: A group of rules that together form a single route, i.e. UrlRule[]. */ class UrlRule { /** * Construct the route with all required properties. * @param rule - The rule to match. */ constructor(rule) { this.identifier = rule.identifier; this.protocol = rule.protocol; this.hostName = rule.hostName; if ((0, optional_1.isSome)(rule.path)) { this.pathComponents = rule.path.split("/").filter((component) => component.length > 0); this.pathParameterMap = makePathParameterMapping(this.pathComponents); } else { this.pathComponents = undefined; this.pathParameterMap = undefined; } this.pathExtension = rule.pathExtension; this.query = parseQuery(rule.query); this.hash = rule.hash; this.regex = rule.regex; if ((0, optional_1.isSome)(rule.exclusions)) { this.exclusions = rule.exclusions.map(function (ex) { return new UrlRule(ex); }); } else { this.exclusions = undefined; } } /** * Checks whether or not the route matches a given URL. * @param url - The URL to check against. * @returns true if the route matches `urls`; false otherwise. * * @deprecated prefer `match` to have access to regex match groups */ matches(url) { return (0, optional_1.isSome)(this.match(url)); } /** * Extract information from a matching url. * @param matchingUrl - The url to extract parameters from. * @returns `Parameters` extracted from `matchingUrl` * @remarks This function is only valid when `this.matches(matchingUrl) === true`. */ extractParameters(matchingUrl) { var _a; const parameters = {}; if ((0, optional_1.isSome)(this.pathComponents) && (0, optional_1.isSome)(this.pathParameterMap)) { const urlPathComponents = matchingUrl.pathComponents(); for (const internalKey of Object.keys(this.pathParameterMap)) { const externalKey = getPathComponentParameter(internalKey); const index = this.pathParameterMap[internalKey]; parameters[externalKey] = decodeURIComponent(urlPathComponents[index]); } } if ((0, optional_1.isSome)(this.query)) { for (const param of this.query) { const queryParam = (_a = matchingUrl.query) === null || _a === void 0 ? void 0 : _a[param.key]; if ((0, optional_1.isSome)(queryParam)) { parameters[param.key] = queryParam; } } } return parameters; } /** * Checks whether or not the route matches a given URL. * @param url - The URL to check against. * @returns an optional `UrlRuleMatchResult` if the route matches `url`. */ match(url) { var _a, _b; let matchGroups = null; if ((0, optional_1.isSome)(this.regex)) { if (this.regex.length === 0) { // If the rule specifies regex but does not supply patterns, we need to return false. Otherwise, we will // risk matching against everything. This is because an empty regex with no other rule parameters will // cause us to fallthrough to the end and match against all URLs. return null; } let didMatchRegex = false; for (const regexPattern of this.regex) { const execResult = regexPattern.exec(url.toString()); if (execResult !== null) { // If we match against any of regex patterns, then we should proceed. // If no matches are found, then this rule is not matched. didMatchRegex = true; matchGroups = (_a = execResult.groups) !== null && _a !== void 0 ? _a : null; break; } } if (!didMatchRegex) { return null; } } if ((0, optional_1.isSome)(this.protocol) && url.protocol !== this.protocol) { return null; } if ((0, optional_1.isSome)(this.hostName) && url.host !== this.hostName) { return null; } if ((0, optional_1.isSome)(this.pathComponents)) { const rulePathComponents = this.pathComponents; const urlPathComponents = url.pathComponents(); if (rulePathComponents.length !== urlPathComponents.length) { return null; } // We're iterating two arrays here, an old style for-loop is appropriate const length = rulePathComponents.length; for (let i = 0; i < length; i += 1) { const ruleComponent = rulePathComponents[i]; if (isPathComponentParameter(ruleComponent)) { // component parameters always match continue; } const urlComponent = urlPathComponents[i]; if (ruleComponent !== urlComponent) { return null; } } } if ((0, optional_1.isSome)(this.pathExtension)) { if (url.pathExtension() !== this.pathExtension) { return null; } } if ((0, optional_1.isSome)(this.query)) { for (const param of this.query) { const value = (_b = url.query) === null || _b === void 0 ? void 0 : _b[param.key]; if ((0, optional_1.isNothing)(value) && !param.optional) { return null; } if ((0, optional_1.isSome)(param.value) && param.value !== value) { return null; } } } if ((0, optional_1.isSome)(this.hash) && url.hash !== this.hash) { return null; } if ((0, optional_1.isSome)(this.exclusions)) { for (const exclusionRule of this.exclusions) { if ((0, optional_1.isSome)(exclusionRule.exclusions)) { throw Error("Matching exclusion rules with further exclusion rules may introduce significant code-complexity and/or reduce the ease with which developers are able to reason about your desired goals. Are there any simpler options?"); } if ((0, optional_1.isSome)(exclusionRule.match(url))) { return null; } } } const parameters = this.extractParameters(url); return { parameters, matchGroups, }; } } exports.UrlRule = UrlRule; /** * `UrlRouter` manages a set of url rule templates to allow `urls` to serve as keys for different associated objects (like Builders). * * @remarks This is replaces old `UrlRouter` as a synchronous way match route URLs to handlers. In contrast to the previous implementation, * it maps entire objects (containing related async handlers and properties) to urls. */ class UrlRouter { /** * Constructs an empty URL router object. */ constructor() { this.routeMappings = []; } /** * Register a new route defined by a set of definitions and object on the router. * @param routeDefinitions - The definitions of rules to register. * @param object - The object for the rule. */ associate(routeDefinitions, object) { const route = []; for (const definition of routeDefinitions) { route.push(new UrlRule(definition)); } this.routeMappings.push({ route: route, object: object }); } /** * Resolve given url to associated object, if any exist. Rules will be evaluated * in the order they are added using the `associate` function. Evaluation will stop * after any rule matches. * @param urlOrString - URL or string representation of url to resolve objects for. * @returns `UrlRouterResult` containing url, extracted parameters, and associated object, or `null` if no match was found. */ routedObjectForUrl(urlOrString) { var _a; const url = typeof urlOrString === "string" ? new urls.URL(urlOrString) : urlOrString; for (const mapping of this.routeMappings) { for (const rule of mapping.route) { const matchResult = rule.match(url); if ((0, optional_1.isSome)(matchResult)) { return { normalizedUrl: url, parameters: matchResult.parameters, object: mapping.object, matchedRuleIdentifier: (_a = rule.identifier) !== null && _a !== void 0 ? _a : null, regexMatchGroups: matchResult.matchGroups, }; } } } // No match. Still return a result with normalized url. return { normalizedUrl: url, parameters: null, object: null, matchedRuleIdentifier: null, regexMatchGroups: null, }; } } exports.UrlRouter = UrlRouter; // endregion //# sourceMappingURL=routing-components.js.map