"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