/** * Created by km on 11/16/16. */ import * as urls from "../network/urls"; import * as urlUtil from "./url-util"; // 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) { const parameterStartIndex = pathComponent.indexOf("{"); const parameterEndIndex = pathComponent.indexOf("}"); return parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex + 1; } /** * 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) { const parameterStartIndex = pathComponent.indexOf("{"); const parameterEndIndex = pathComponent.indexOf("}"); const pathHasRequiredCurlyBraces = parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex; return pathHasRequiredCurlyBraces ? pathComponent.substring(parameterStartIndex + 1, parameterEndIndex) : pathComponent; } /** * Extracts the value from a path component, for a given internal key example: * Path Component: "id123456" * Internal Key: "id{id}" * Return Value: "123456" * @param pathComponent A _pathComponents component surrounded by curly braces. * @returns The parameter contained in the component. */ export function getPathComponentParameterValueUsingInternalKey(pathComponent, internalKey) { const valueStartIndex = internalKey.indexOf("{"); const valueEndIndex = pathComponent.length - (internalKey.length - (internalKey.indexOf("}") + 1)); const pathHasRequiredCurlyBraces = valueStartIndex >= 0 && valueEndIndex > valueStartIndex; return pathHasRequiredCurlyBraces ? pathComponent.substring(valueStartIndex, valueEndIndex) : pathComponent; } /** * 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; } /** * Normalizes a given protocol string for matching. * @param protocol The protocol to match against. * @returns The `protocol` with colon added if needed. */ function normalizeProtocol(protocol) { // An empty string is falsy. if (protocol === null || protocol === undefined) { return null; } return protocol; } /** * Creates `UrlRouteQuery` objects from substring of url. * ? = optional * -caseInsensitive = case insensitive * @param parameters strings of form `[?][-i]=`. * @returns Array of `UrlRouteQuery` objects. */ function parseQuery(parameters) { const parsedQuery = []; if (!parameters) { return parsedQuery; } for (const param of parameters) { const parts = param.split("="); let key = parts[0]; const optional = key.indexOf("?") !== -1; key = key.replace("?", ""); const caseInsensitive = key.indexOf("-caseInsensitive") !== -1; key = key.replace("-caseInsensitive", ""); let value = null; if (parts.length > 1) { value = decodeURIComponent(parts[1]); } parsedQuery.push({ key, value, optional, caseInsensitive, }); } return parsedQuery; } // endregion // region Url Rule /** * 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[]. */ export class UrlRule { /** * Construct the route with all required properties. * @param rule The rule to match. */ constructor(rule) { this.identifier = rule.identifier; this._protocol = normalizeProtocol(rule.protocol); this._hostName = rule.hostName; if (rule.path) { this._pathComponents = rule.path.split("/").filter((component) => component.length > 0); this._pathParameterMap = makePathParameterMapping(this._pathComponents); } else { this._pathComponents = null; this._pathParameterMap = null; } this._pathExtension = rule.pathExtension; this._query = parseQuery(rule.query); this._hash = rule.hash; this._regex = rule.regex; if (rule.exclusions) { this._exclusions = rule.exclusions.map(function (ex) { return new UrlRule(ex); }); } else { this._exclusions = null; } } /** * 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. */ matches(url) { var _a, _b; if (this._regex) { if (!this._regex.length) { // 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 false; } let didMatchRegex = false; for (const regexPattern of this._regex) { if (regexPattern.test(url.toString())) { // 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; break; } } if (!didMatchRegex) { return false; } } if (this._protocol && url.protocol !== this._protocol) { return false; } if (this._hostName && url.host !== this._hostName) { return false; } if (this._pathComponents) { const rulePathComponents = this._pathComponents; const urlPathComponents = url.pathComponents(); if (rulePathComponents.length !== urlPathComponents.length) { return false; } // We're iterating two arrays here, an old style for-loop is appropriate const length = rulePathComponents.length; for (let i = 0; i < length; i++) { const ruleComponent = rulePathComponents[i]; if (isPathComponentParameter(ruleComponent)) { // component parameters always match continue; } const urlComponent = urlPathComponents[i]; if (ruleComponent !== urlComponent) { return false; } } } if (this._pathExtension) { if (url.pathExtension() !== this._pathExtension) { return false; } } if (this._query) { for (const param of this._query) { let value; if (param.caseInsensitive) { for (const [queryKey, queryValue] of Object.entries((_a = url.query) !== null && _a !== void 0 ? _a : {})) { if (param.key.toLocaleLowerCase() === queryKey.toLocaleLowerCase()) { value = queryValue; } } } else { value = (_b = url.query) === null || _b === void 0 ? void 0 : _b[param.key]; } if (!value && !param.optional) { return false; } if (param.value && param.value !== value) { return false; } } } if (this._hash && url.hash !== this._hash) { return false; } if (this._exclusions) { for (const exclusionRule of this._exclusions) { if (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 (exclusionRule.matches(url)) { return false; } } } return true; } /** * Extract information from a matching url. * @param matchingUrl The url to extract parameters from. * @returns `Parameters` extracted from `matchingUrl` * @note This function is only valid when `this.matches(matchingUrl) === true`. */ extractParameters(matchingUrl) { var _a, _b; const parameters = {}; if (this._pathComponents !== null && this._pathParameterMap !== null) { const urlPathComponents = matchingUrl.pathComponents(); for (const internalKey of Object.keys(this._pathParameterMap)) { const externalKey = getPathComponentParameter(internalKey); const index = this._pathParameterMap[internalKey]; const parameterValue = getPathComponentParameterValueUsingInternalKey(urlPathComponents[index], internalKey); parameters[externalKey] = decodeURIComponent(parameterValue); } } if (this._query) { for (const param of this._query) { parameters[param.key] = (_b = (_a = matchingUrl.query) === null || _a === void 0 ? void 0 : _a[param.key]) !== null && _b !== void 0 ? _b : undefined; } } return parameters; } } /** * `UrlRouter` manages a set of url rule templates to allow `urls` to serve as keys for different associated objects (like Builders). * * @note 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. */ export 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. * @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) { let url = typeof urlOrString === "string" ? new urls.URL(urlOrString) : urlOrString; url = urlUtil.normalizedAppStoreUrl(url); url = urlUtil.normalizedActionUrl(url); for (const mapping of this._routeMappings) { for (const rule of mapping.route) { if (rule.matches(url)) { return { normalizedUrl: url, parameters: rule.extractParameters(url), object: mapping.object, matchedRuleIdentifier: rule.identifier, }; } } } // No match. Still return a result with normalized url. return { normalizedUrl: url, parameters: null, object: null, matchedRuleIdentifier: null, }; } } // endregion //# sourceMappingURL=routing-components.js.map