routing-components.js
1 "use strict"; 2 Object.defineProperty(exports, "__esModule", { value: true }); 3 exports.UrlRouter = exports.UrlRule = void 0; 4 const optional_1 = require("../types/optional"); 5 const urls = require("../util/urls"); 6 // endregion 7 // region private URLRule helpers. 8 /** 9 * Checks whether or not a given pathComponents component contains a parameter. 10 * @param pathComponent - The pathComponents component to check. 11 * @returns true if the pathComponents component is surrounded by curly braces; false otherwise. 12 */ 13 function isPathComponentParameter(pathComponent) { 14 return pathComponent.startsWith("{") && pathComponent.endsWith("}"); 15 } 16 /** 17 * Extracts the parameter contained in a pathComponents component. 18 * @param pathComponent - A pathComponents component surrounded by curly braces. 19 * @returns The parameter contained in the component. 20 */ 21 function getPathComponentParameter(pathComponent) { 22 return pathComponent.replace("{", "").replace("}", ""); 23 } 24 /** 25 * Creates a mapping from key to pathComponents component index 26 * for efficiently extracting parameters from a pathComponents. 27 * @param rulePath - The pathComponents to create a mapping for. 28 * @returns A map of keys to pathComponents component indexes. 29 */ 30 function makePathParameterMapping(rulePath) { 31 const mapping = {}; 32 rulePath.forEach((ruleComponent, index) => { 33 if (isPathComponentParameter(ruleComponent)) { 34 mapping[ruleComponent] = index; 35 } 36 }); 37 return mapping; 38 } 39 /** 40 * Creates `UrlRouteQuery` objects from substring of url. 41 * @param parameters - strings of form `<key>[?]=<value>`. 42 * @returns Array of `UrlRouteQuery` objects. 43 */ 44 function parseQuery(parameters) { 45 const parsedQuery = []; 46 if ((0, optional_1.isNothing)(parameters)) { 47 return parsedQuery; 48 } 49 for (const param of parameters) { 50 const parts = param.split("="); 51 let key = parts[0]; 52 const optional = key.includes("?"); 53 key = key.replace("?", ""); 54 let value = null; 55 if (parts.length > 1) { 56 value = decodeURIComponent(parts[1]); 57 } 58 parsedQuery.push({ 59 key, 60 value, 61 optional, 62 }); 63 } 64 return parsedQuery; 65 } 66 /** 67 * The `UrlRule` class extracts the pattern format from `UrlRuleDefinition`s, and encapsulates 68 * the information needed to match against a candidate URL and extract parameters from it. 69 * 70 * The terminology here is: 71 * - rule: A specific url pattern. 72 * - route: A group of rules that together form a single route, i.e. UrlRule[]. 73 */ 74 class UrlRule { 75 /** 76 * Construct the route with all required properties. 77 * @param rule - The rule to match. 78 */ 79 constructor(rule) { 80 this.identifier = rule.identifier; 81 this.protocol = rule.protocol; 82 this.hostName = rule.hostName; 83 if ((0, optional_1.isSome)(rule.path)) { 84 this.pathComponents = rule.path.split("/").filter((component) => component.length > 0); 85 this.pathParameterMap = makePathParameterMapping(this.pathComponents); 86 } 87 else { 88 this.pathComponents = undefined; 89 this.pathParameterMap = undefined; 90 } 91 this.pathExtension = rule.pathExtension; 92 this.query = parseQuery(rule.query); 93 this.hash = rule.hash; 94 this.regex = rule.regex; 95 if ((0, optional_1.isSome)(rule.exclusions)) { 96 this.exclusions = rule.exclusions.map(function (ex) { 97 return new UrlRule(ex); 98 }); 99 } 100 else { 101 this.exclusions = undefined; 102 } 103 } 104 /** 105 * Checks whether or not the route matches a given URL. 106 * @param url - The URL to check against. 107 * @returns true if the route matches `urls`; false otherwise. 108 * 109 * @deprecated prefer `match` to have access to regex match groups 110 */ 111 matches(url) { 112 return (0, optional_1.isSome)(this.match(url)); 113 } 114 /** 115 * Extract information from a matching url. 116 * @param matchingUrl - The url to extract parameters from. 117 * @returns `Parameters` extracted from `matchingUrl` 118 * @remarks This function is only valid when `this.matches(matchingUrl) === true`. 119 */ 120 extractParameters(matchingUrl) { 121 var _a; 122 const parameters = {}; 123 if ((0, optional_1.isSome)(this.pathComponents) && (0, optional_1.isSome)(this.pathParameterMap)) { 124 const urlPathComponents = matchingUrl.pathComponents(); 125 for (const internalKey of Object.keys(this.pathParameterMap)) { 126 const externalKey = getPathComponentParameter(internalKey); 127 const index = this.pathParameterMap[internalKey]; 128 parameters[externalKey] = decodeURIComponent(urlPathComponents[index]); 129 } 130 } 131 if ((0, optional_1.isSome)(this.query)) { 132 for (const param of this.query) { 133 const queryParam = (_a = matchingUrl.query) === null || _a === void 0 ? void 0 : _a[param.key]; 134 if ((0, optional_1.isSome)(queryParam)) { 135 parameters[param.key] = queryParam; 136 } 137 } 138 } 139 return parameters; 140 } 141 /** 142 * Checks whether or not the route matches a given URL. 143 * @param url - The URL to check against. 144 * @returns an optional `UrlRuleMatchResult` if the route matches `url`. 145 */ 146 match(url) { 147 var _a, _b; 148 let matchGroups = null; 149 if ((0, optional_1.isSome)(this.regex)) { 150 if (this.regex.length === 0) { 151 // If the rule specifies regex but does not supply patterns, we need to return false. Otherwise, we will 152 // risk matching against everything. This is because an empty regex with no other rule parameters will 153 // cause us to fallthrough to the end and match against all URLs. 154 return null; 155 } 156 let didMatchRegex = false; 157 for (const regexPattern of this.regex) { 158 const execResult = regexPattern.exec(url.toString()); 159 if (execResult !== null) { 160 // If we match against any of regex patterns, then we should proceed. 161 // If no matches are found, then this rule is not matched. 162 didMatchRegex = true; 163 matchGroups = (_a = execResult.groups) !== null && _a !== void 0 ? _a : null; 164 break; 165 } 166 } 167 if (!didMatchRegex) { 168 return null; 169 } 170 } 171 if ((0, optional_1.isSome)(this.protocol) && url.protocol !== this.protocol) { 172 return null; 173 } 174 if ((0, optional_1.isSome)(this.hostName) && url.host !== this.hostName) { 175 return null; 176 } 177 if ((0, optional_1.isSome)(this.pathComponents)) { 178 const rulePathComponents = this.pathComponents; 179 const urlPathComponents = url.pathComponents(); 180 if (rulePathComponents.length !== urlPathComponents.length) { 181 return null; 182 } 183 // We're iterating two arrays here, an old style for-loop is appropriate 184 const length = rulePathComponents.length; 185 for (let i = 0; i < length; i += 1) { 186 const ruleComponent = rulePathComponents[i]; 187 if (isPathComponentParameter(ruleComponent)) { 188 // component parameters always match 189 continue; 190 } 191 const urlComponent = urlPathComponents[i]; 192 if (ruleComponent !== urlComponent) { 193 return null; 194 } 195 } 196 } 197 if ((0, optional_1.isSome)(this.pathExtension)) { 198 if (url.pathExtension() !== this.pathExtension) { 199 return null; 200 } 201 } 202 if ((0, optional_1.isSome)(this.query)) { 203 for (const param of this.query) { 204 const value = (_b = url.query) === null || _b === void 0 ? void 0 : _b[param.key]; 205 if ((0, optional_1.isNothing)(value) && !param.optional) { 206 return null; 207 } 208 if ((0, optional_1.isSome)(param.value) && param.value !== value) { 209 return null; 210 } 211 } 212 } 213 if ((0, optional_1.isSome)(this.hash) && url.hash !== this.hash) { 214 return null; 215 } 216 if ((0, optional_1.isSome)(this.exclusions)) { 217 for (const exclusionRule of this.exclusions) { 218 if ((0, optional_1.isSome)(exclusionRule.exclusions)) { 219 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?"); 220 } 221 if ((0, optional_1.isSome)(exclusionRule.match(url))) { 222 return null; 223 } 224 } 225 } 226 const parameters = this.extractParameters(url); 227 return { 228 parameters, 229 matchGroups, 230 }; 231 } 232 } 233 exports.UrlRule = UrlRule; 234 /** 235 * `UrlRouter` manages a set of url rule templates to allow `urls` to serve as keys for different associated objects (like Builders). 236 * 237 * @remarks This is replaces old `UrlRouter` as a synchronous way match route URLs to handlers. In contrast to the previous implementation, 238 * it maps entire objects (containing related async handlers and properties) to urls. 239 */ 240 class UrlRouter { 241 /** 242 * Constructs an empty URL router object. 243 */ 244 constructor() { 245 this.routeMappings = []; 246 } 247 /** 248 * Register a new route defined by a set of definitions and object on the router. 249 * @param routeDefinitions - The definitions of rules to register. 250 * @param object - The object for the rule. 251 */ 252 associate(routeDefinitions, object) { 253 const route = []; 254 for (const definition of routeDefinitions) { 255 route.push(new UrlRule(definition)); 256 } 257 this.routeMappings.push({ route: route, object: object }); 258 } 259 /** 260 * Resolve given url to associated object, if any exist. Rules will be evaluated 261 * in the order they are added using the `associate` function. Evaluation will stop 262 * after any rule matches. 263 * @param urlOrString - URL or string representation of url to resolve objects for. 264 * @returns `UrlRouterResult` containing url, extracted parameters, and associated object, or `null` if no match was found. 265 */ 266 routedObjectForUrl(urlOrString) { 267 var _a; 268 const url = typeof urlOrString === "string" ? new urls.URL(urlOrString) : urlOrString; 269 for (const mapping of this.routeMappings) { 270 for (const rule of mapping.route) { 271 const matchResult = rule.match(url); 272 if ((0, optional_1.isSome)(matchResult)) { 273 return { 274 normalizedUrl: url, 275 parameters: matchResult.parameters, 276 object: mapping.object, 277 matchedRuleIdentifier: (_a = rule.identifier) !== null && _a !== void 0 ? _a : null, 278 regexMatchGroups: matchResult.matchGroups, 279 }; 280 } 281 } 282 } 283 // No match. Still return a result with normalized url. 284 return { 285 normalizedUrl: url, 286 parameters: null, 287 object: null, 288 matchedRuleIdentifier: null, 289 regexMatchGroups: null, 290 }; 291 } 292 } 293 exports.UrlRouter = UrlRouter; 294 // endregion 295 //# sourceMappingURL=routing-components.js.map