/ node_modules / @jet / environment / routing / routing-components.js
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