"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateOverrides = exports.InputParameters = void 0;
const util_1 = require("../util");
const error_1 = require("./error");
/**
 * Error thrown when the validation of a request's params fails.
 */
class InputValidationError extends error_1.AdapterError {
    constructor(message) {
        super({
            statusCode: 400,
            message,
        });
    }
}
/**
 * Error thrown when the validation of the input parameters definition fails.
 */
class InputParametersDefinitionError extends Error {
}
/**
 * This class encapsulates logic for a single input parameter, taking its definition
 * and performing validations (both on the definition on construction, and params when prompted).
 */
class ProcessedParam {
    constructor(name, definition) {
        this.name = name;
        this.definition = definition;
        this.aliases = [this.name, ...(this.definition.aliases || [])];
        this.type =
            typeof definition.type === 'object' ? new InputParameters(definition.type) : definition.type;
        if (definition.options) {
            this.options = new Set(definition.options);
        }
        this.validateDefinition();
    }
    /** Util method to throw a definition error prefixed with this param name */
    definitionError(message) {
        return new InputParametersDefinitionError(`[Param: ${this.name}] ${message}`);
    }
    /** Util method to throw a validation error prefixed with this param name */
    validationError(message) {
        return new InputValidationError(`[Param: ${this.name}] ${message}`);
    }
    /**
     * Validates the definition that this parameter has been constructed with.
     */
    validateDefinition() {
        // Check that there are no repeated aliases
        if ((0, util_1.hasRepeatedValues)(this.aliases)) {
            throw this.definitionError(`There are repeated aliases for input param ${this.name}: ${this.aliases}`);
        }
        // Check that if options are specified it has at least one entry
        if (this.definition.options?.length === 0) {
            throw this.definitionError(`The options array must contain at least one option`);
        }
        // Check that there are no repeated options
        if (this.options && this.definition.options?.length !== this.options.size) {
            throw this.definitionError(`There are duplicates in the specified options: ${this.definition.options}`);
        }
    }
    /**
     * Validates an incoming adapter request's input params and
     * performs all necessary checks and modifications.
     *
     * @param input - the input data from an incoming adapter request
     * @returns - the validated data
     */
    validateInput(input) {
        if (this.definition.required && input == null) {
            throw this.validationError('param is required but no value was provided');
        }
        if (this.definition.default != null && input == null) {
            return this.definition.default;
        }
        if (this.definition.array) {
            if (!(Array.isArray(input) && input.length >= 0)) {
                if (this.definition.required || input != null) {
                    throw this.validationError('input value must be a non-empty array');
                }
                else {
                    return [];
                }
            }
            // Validate each value from the array individually
            return input.map((item) => this.validateInputType(item));
        }
        // We already know this won't be an array, but the generics are too complex for typescript
        // to infer by itself so we cast manually
        return this.validateInputType(input);
    }
    /**
     * Validates a single value from the incoming params.
     *
     * @param input - a single value from the request params object
     * @returns - the validated input data
     */
    validateInputType(input) {
        // If we're here we've already checked this is not required
        if (input == null) {
            return;
        }
        // If the type is a nested input params object, use that to validate
        if (this.type instanceof InputParameters) {
            return this.type.validateInput(input);
        }
        // If the param has specified options, check that the input is one of them.
        // In this case we don't need to check the type, since the options will do that for us
        if (this.options && !this.options.has(input)) {
            throw this.validationError(`input is not one of valid options (${this.definition.options})`);
        }
        else if (typeof input !== this.definition.type) {
            throw this.validationError(`input type is not the expected one (${this.type})`);
        }
        // If no validations failed and no defaults / modifications were applied, use the original input
        return input;
    }
}
class InputParameters {
    constructor(definition, examples) {
        this.definition = definition;
        this.examples = examples;
        this.params = Object.entries(this.definition).map(([name, param]) => new ProcessedParam(name, param));
        // Check that all options match param type
        // Check that defaults matches param validation
        this.validateDefinition();
    }
    /**
     * Validates the entire definitions object provided to the constructor.
     */
    validateDefinition() {
        const paramNames = new Set(this.params.map((p) => p.name));
        // Check that aliases don't clash with other properties
        if ((0, util_1.hasRepeatedValues)(this.params.map((p) => p.aliases).flat())) {
            throw new InputParametersDefinitionError('There are clashes in property names and aliases, check that they are all unique');
        }
        for (const param of this.params) {
            // Check that all dependencies reference valid options
            for (const dependency of param.definition.dependsOn || []) {
                if (!paramNames.has(dependency)) {
                    throw new InputParametersDefinitionError(`Param "${param.name}" depends on non-existent param "${dependency}"`);
                }
                if (this.definition[dependency].required) {
                    throw new InputParametersDefinitionError(`Param "${param.name}" has an unnecessary dependency on "${dependency}" (dependency is always required)`);
                }
            }
            for (const exclusion of param.definition.exclusive || []) {
                if (!paramNames.has(exclusion)) {
                    throw new InputParametersDefinitionError(`Param "${param.name}" excludes non-existent param "${exclusion}"`);
                }
                if (this.definition[exclusion].required) {
                    throw new InputParametersDefinitionError(`Param "${param.name}" excludes required (i.e. always present) param "${exclusion}"`);
                }
            }
        }
    }
    /**
     * Validates an incoming adapter request's input params and
     * performs all necessary checks and modifications.
     *
     * @param input - the input data from an incoming adapter request
     * @returns - the validated data
     */
    validateInput(rawData) {
        if (typeof rawData !== 'object' || rawData == null) {
            throw new InputValidationError('Input for input parameters should be an object');
        }
        const data = rawData;
        const validated = {};
        // Validate each param individually
        for (const param of this.params) {
            let value;
            for (const alias of param.aliases) {
                if (data[alias] == null) {
                    continue;
                }
                if (value) {
                    throw new InputValidationError(`Parameter "${param.name}" is specified more than once (aliases: ${param.aliases})`);
                }
                value = data[alias];
            }
            // Perform all validations for the individual param value
            validated[param.name] = param.validateInput(value);
        }
        // We iterate again, now with the complete validated obj to check for dependencies and exclusions
        for (const param of this.params) {
            if (validated[param.name] == null) {
                continue;
            }
            if (param.definition.dependsOn?.some((d) => validated[d] == null)) {
                throw new InputValidationError(`Parameter "${param.name}" is missing dependencies (${param.definition.dependsOn})`);
            }
            if (param.definition.exclusive?.some((d) => validated[d] != null)) {
                throw new InputValidationError(`Parameter "${param.name}" cannot be present at the same time as exclusions (${param.definition.exclusive})`);
            }
        }
        return validated;
    }
}
exports.InputParameters = InputParameters;
/**
 * Validates that the overrides object in a request (if present) is correct.
 *
 * @param input - a request's input data, which may contain overrides
 * @returns nothing, only throws if an error is found
 */
const validateOverrides = (input) => {
    if (!input.overrides) {
        // Nothing to validate!
        return;
    }
    if (typeof input.overrides !== 'object') {
        throw new InputValidationError('Overrides should be an object');
    }
    for (const adapterName in input.overrides) {
        const overrides = input.overrides[adapterName];
        if (typeof overrides !== 'object') {
            throw new InputValidationError(`Overrides for adapter "${adapterName}" should be an object`);
        }
        for (const symbol in overrides) {
            const override = overrides[symbol];
            if (typeof symbol !== 'string' || typeof override !== 'string') {
                throw new InputValidationError(`Overrides should map strings to strings, got ${symbol} to ${override}`);
            }
        }
    }
};
exports.validateOverrides = validateOverrides;
//# sourceMappingURL=input-params.js.map