1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
"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 `<key>[?]=<value>`.
* @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
|