uriTemplate.js•8.79 kB
"use strict";
// Claude-authored implementation of RFC 6570 URI Templates
Object.defineProperty(exports, "__esModule", { value: true });
exports.UriTemplate = void 0;
const MAX_TEMPLATE_LENGTH = 1000000; // 1MB
const MAX_VARIABLE_LENGTH = 1000000; // 1MB
const MAX_TEMPLATE_EXPRESSIONS = 10000;
const MAX_REGEX_LENGTH = 1000000; // 1MB
class UriTemplate {
    /**
     * Returns true if the given string contains any URI template expressions.
     * A template expression is a sequence of characters enclosed in curly braces,
     * like {foo} or {?bar}.
     */
    static isTemplate(str) {
        // Look for any sequence of characters between curly braces
        // that isn't just whitespace
        return /\{[^}\s]+\}/.test(str);
    }
    static validateLength(str, max, context) {
        if (str.length > max) {
            throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
        }
    }
    constructor(template) {
        UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template");
        this.template = template;
        this.parts = this.parse(template);
    }
    toString() {
        return this.template;
    }
    parse(template) {
        const parts = [];
        let currentText = "";
        let i = 0;
        let expressionCount = 0;
        while (i < template.length) {
            if (template[i] === "{") {
                if (currentText) {
                    parts.push(currentText);
                    currentText = "";
                }
                const end = template.indexOf("}", i);
                if (end === -1)
                    throw new Error("Unclosed template expression");
                expressionCount++;
                if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
                    throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
                }
                const expr = template.slice(i + 1, end);
                const operator = this.getOperator(expr);
                const exploded = expr.includes("*");
                const names = this.getNames(expr);
                const name = names[0];
                // Validate variable name length
                for (const name of names) {
                    UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, "Variable name");
                }
                parts.push({ name, operator, names, exploded });
                i = end + 1;
            }
            else {
                currentText += template[i];
                i++;
            }
        }
        if (currentText) {
            parts.push(currentText);
        }
        return parts;
    }
    getOperator(expr) {
        const operators = ["+", "#", ".", "/", "?", "&"];
        return operators.find((op) => expr.startsWith(op)) || "";
    }
    getNames(expr) {
        const operator = this.getOperator(expr);
        return expr
            .slice(operator.length)
            .split(",")
            .map((name) => name.replace("*", "").trim())
            .filter((name) => name.length > 0);
    }
    encodeValue(value, operator) {
        UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, "Variable value");
        if (operator === "+" || operator === "#") {
            return encodeURI(value);
        }
        return encodeURIComponent(value);
    }
    expandPart(part, variables) {
        if (part.operator === "?" || part.operator === "&") {
            const pairs = part.names
                .map((name) => {
                const value = variables[name];
                if (value === undefined)
                    return "";
                const encoded = Array.isArray(value)
                    ? value.map((v) => this.encodeValue(v, part.operator)).join(",")
                    : this.encodeValue(value.toString(), part.operator);
                return `${name}=${encoded}`;
            })
                .filter((pair) => pair.length > 0);
            if (pairs.length === 0)
                return "";
            const separator = part.operator === "?" ? "?" : "&";
            return separator + pairs.join("&");
        }
        if (part.names.length > 1) {
            const values = part.names
                .map((name) => variables[name])
                .filter((v) => v !== undefined);
            if (values.length === 0)
                return "";
            return values.map((v) => (Array.isArray(v) ? v[0] : v)).join(",");
        }
        const value = variables[part.name];
        if (value === undefined)
            return "";
        const values = Array.isArray(value) ? value : [value];
        const encoded = values.map((v) => this.encodeValue(v, part.operator));
        switch (part.operator) {
            case "":
                return encoded.join(",");
            case "+":
                return encoded.join(",");
            case "#":
                return "#" + encoded.join(",");
            case ".":
                return "." + encoded.join(".");
            case "/":
                return "/" + encoded.join("/");
            default:
                return encoded.join(",");
        }
    }
    expand(variables) {
        let result = "";
        let hasQueryParam = false;
        for (const part of this.parts) {
            if (typeof part === "string") {
                result += part;
                continue;
            }
            const expanded = this.expandPart(part, variables);
            if (!expanded)
                continue;
            // Convert ? to & if we already have a query parameter
            if ((part.operator === "?" || part.operator === "&") && hasQueryParam) {
                result += expanded.replace("?", "&");
            }
            else {
                result += expanded;
            }
            if (part.operator === "?" || part.operator === "&") {
                hasQueryParam = true;
            }
        }
        return result;
    }
    escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }
    partToRegExp(part) {
        const patterns = [];
        // Validate variable name length for matching
        for (const name of part.names) {
            UriTemplate.validateLength(name, MAX_VARIABLE_LENGTH, "Variable name");
        }
        if (part.operator === "?" || part.operator === "&") {
            for (let i = 0; i < part.names.length; i++) {
                const name = part.names[i];
                const prefix = i === 0 ? "\\" + part.operator : "&";
                patterns.push({
                    pattern: prefix + this.escapeRegExp(name) + "=([^&]+)",
                    name,
                });
            }
            return patterns;
        }
        let pattern;
        const name = part.name;
        switch (part.operator) {
            case "":
                pattern = part.exploded ? "([^/]+(?:,[^/]+)*)" : "([^/,]+)";
                break;
            case "+":
            case "#":
                pattern = "(.+)";
                break;
            case ".":
                pattern = "\\.([^/,]+)";
                break;
            case "/":
                pattern = "/" + (part.exploded ? "([^/]+(?:,[^/]+)*)" : "([^/,]+)");
                break;
            default:
                pattern = "([^/]+)";
        }
        patterns.push({ pattern, name });
        return patterns;
    }
    match(uri) {
        UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, "URI");
        let pattern = "^";
        const names = [];
        for (const part of this.parts) {
            if (typeof part === "string") {
                pattern += this.escapeRegExp(part);
            }
            else {
                const patterns = this.partToRegExp(part);
                for (const { pattern: partPattern, name } of patterns) {
                    pattern += partPattern;
                    names.push({ name, exploded: part.exploded });
                }
            }
        }
        pattern += "$";
        UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, "Generated regex pattern");
        const regex = new RegExp(pattern);
        const match = uri.match(regex);
        if (!match)
            return null;
        const result = {};
        for (let i = 0; i < names.length; i++) {
            const { name, exploded } = names[i];
            const value = match[i + 1];
            const cleanName = name.replace("*", "");
            if (exploded && value.includes(",")) {
                result[cleanName] = value.split(",");
            }
            else {
                result[cleanName] = value;
            }
        }
        return result;
    }
}
exports.UriTemplate = UriTemplate;
//# sourceMappingURL=uriTemplate.js.map