summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/foundation/routing/routing-components.js
blob: 28fdc483abf10ec391e304c6df16ec4318cdf570 (plain)
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/**
 * 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 `<key>[?][-i]=<value>`.
 * @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