"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.unexpectedNull = exports.catchingContext = exports.context = exports.recordValidationIncidents = exports.endContext = exports.getContextNames = exports.beginContext = exports.messageForRecoveryAction = exports.isValidatable = exports.unexpectedType = exports.extendedTypeof = void 0;
const optional_1 = require("../types/optional");
/**
 * Returns a string containing the type of a given value.
 * This function augments the built in `typeof` operator
 * to return sensible values for arrays and null values.
 *
 * @privateRemarks
 * This function is exported for testing.
 *
 * @param value - The value to find the type of.
 * @returns A string containing the type of `value`.
 */
function extendedTypeof(value) {
    if (Array.isArray(value)) {
        return "array";
    }
    else if (value === null) {
        return "null";
    }
    else {
        return typeof value;
    }
}
exports.extendedTypeof = extendedTypeof;
/**
 * Reports a non-fatal validation failure, logging a message to the console.
 * @param recovery -    The recovery action taken when the bad type was found.
 * @param expected -    The expected type of the value.
 * @param actual -      The actual value.
 * @param pathString -  A string containing the path to the value on the object which failed type validation.
 */
function unexpectedType(recovery, expected, actual, pathString) {
    const actualType = extendedTypeof(actual);
    const prettyPath = optional_1.isSome(pathString) && pathString.length > 0 ? pathString : "<this>";
    trackIncident({
        type: "badType",
        expected: expected,
        // Our test assertions are matching the string interpolation of ${actual} value.
        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
        actual: `${actualType} (${actual})`,
        objectPath: prettyPath,
        contextNames: getContextNames(),
        recoveryAction: recovery,
        stack: new Error().stack,
    });
}
exports.unexpectedType = unexpectedType;
// endregion
/**
 * Determines if a given object conforms to the Validatable interface
 * @param possibleValidatable - An object that might be considered validatable
 *
 * @returns `true` if it is an instance of Validatable, `false` if not
 */
function isValidatable(possibleValidatable) {
    if (optional_1.isNothing(possibleValidatable)) {
        return false;
    }
    // MAINTAINER'S NOTE: We must check for either the existence of a pre-existing incidents
    //                    property *or* the ability to add one. Failure to do so will cause
    //                    problems for clients that either a) use interfaces to define their
    //                    view models; or b) return collections from their service routes.
    return (Object.prototype.hasOwnProperty.call(possibleValidatable, "$incidents") ||
        Object.isExtensible(possibleValidatable));
}
exports.isValidatable = isValidatable;
/**
 * Returns a developer-readable diagnostic message for a given recovery action.
 * @param action - The recovery action to get the message for.
 * @returns The message for `action`.
 */
function messageForRecoveryAction(action) {
    switch (action) {
        case "coercedValue":
            return "Coerced format";
        case "defaultValue":
            return "Default value used";
        case "ignoredValue":
            return "Ignored value";
        default:
            return "Unknown";
    }
}
exports.messageForRecoveryAction = messageForRecoveryAction;
// region Contexts
/**
 * Shared validation context "stack".
 *
 * Because validation incidents propagate up the context stack,
 * the representation used here is optimized for memory usage.
 * A more literal representation of this would be a singly linked
 * list describing a basic stack, but that will produce a large
 * amount of unnecessary garbage and require copying `incidents`
 * arrays backwards.
 */
const contextState = {
    /// The names of each validation context on the stack.
    nameStack: Array(),
    /// All incidents reported so far. Cleared when the
    /// context stack is emptied.
    incidents: Array(),
    // TODO: Removal of this is being tracked here:
    //     <rdar://problem/35015460> Intro Pricing: Un-suppress missing parent 'offers' error when server address missing key
    /// The paths for incidents we wish to forgo tracking.
    suppressedIncidentPaths: Array(),
};
/**
 * Begin a new validation context with a given name,
 * pushing it onto the validation context stack.
 * @param name - The name for the validation context.
 */
function beginContext(name) {
    contextState.nameStack.push(name);
}
exports.beginContext = beginContext;
/**
 * Traverses the validation context stack and collects all of the context names.
 * @returns The names of all validation contexts on the stack, from oldest to newest.
 */
function getContextNames() {
    if (contextState.nameStack.length === 0) {
        return ["<empty stack>"];
    }
    return contextState.nameStack.slice(0);
}
exports.getContextNames = getContextNames;
/**
 * Ends the current validation context
 */
function endContext() {
    if (contextState.nameStack.length === 0) {
        console.warn("endContext() called without active validation context, ignoring");
    }
    contextState.nameStack.pop();
}
exports.endContext = endContext;
/**
 * Records validation incidents back into an object that implements Validatable.
 *
 * Note: This method has a side-effect that the incident queue and name stack are cleared
 * to prepare for the next thread's invocation.
 *
 * @param possibleValidatable - An object that may conform to Validatable, onto which we
 * want to stash our validation incidents
 */
function recordValidationIncidents(possibleValidatable) {
    if (isValidatable(possibleValidatable)) {
        possibleValidatable.$incidents = contextState.incidents;
    }
    contextState.incidents = [];
    contextState.nameStack = [];
    contextState.suppressedIncidentPaths = [];
}
exports.recordValidationIncidents = recordValidationIncidents;
/**
 * Create a transient validation context, and call a function that will return a value.
 *
 * Prefer this function over manually calling begin/endContext,
 * it is exception safe.
 *
 * @param name - The name of the context
 * @param producer - A function that produces a result
 * @returns <Result> The resulting type
 */
function context(name, producer, suppressingPath) {
    let suppressingName = null;
    if (optional_1.isSome(suppressingPath) && suppressingPath.length > 0) {
        suppressingName = name;
        contextState.suppressedIncidentPaths.push(suppressingPath);
    }
    let result = null;
    try {
        beginContext(name);
        result = producer();
    }
    catch (e) {
        // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
        if (!e.hasThrown) {
            unexpectedType("defaultValue", "no exception", e.message);
            e.hasThrown = true;
        }
        throw e;
    }
    finally {
        if (name === suppressingName) {
            contextState.suppressedIncidentPaths.pop();
        }
        endContext();
    }
    return result;
}
exports.context = context;
/**
 * Create a transient validation context, that catches errors and returns null
 *
 * @param name - The name of the context
 * @param producer - A function that produces a result
 * @param caught - An optional handler to provide a value when an error is caught
 * @returns <Result> The resulting type
 */
function catchingContext(name, producer, caught) {
    let result = null;
    try {
        result = context(name, producer);
    }
    catch (e) {
        result = null;
        if (optional_1.isSome(caught)) {
            result = caught(e);
        }
    }
    return result;
}
exports.catchingContext = catchingContext;
/**
 * Track an incident within the current validation context.
 * @param incident - An incident object describing the problem.
 */
function trackIncident(incident) {
    if (contextState.suppressedIncidentPaths.includes(incident.objectPath)) {
        return;
    }
    contextState.incidents.push(incident);
}
// endregion
// region Nullability
/**
 * Reports a non-fatal error indicating a value was unexpectedly null.
 * @param recovery -      The recovery action taken when the null value was found.
 * @param expected -      The expected type of the value.
 * @param pathString -    A string containing the path to the value on the object which was null.
 */
function unexpectedNull(recovery, expected, pathString) {
    const prettyPath = optional_1.isSome(pathString) && pathString.length > 0 ? pathString : "<this>";
    trackIncident({
        type: "nullValue",
        expected: expected,
        actual: "null",
        objectPath: prettyPath,
        contextNames: getContextNames(),
        recoveryAction: recovery,
        stack: new Error().stack,
    });
}
exports.unexpectedNull = unexpectedNull;
// endregion
//# sourceMappingURL=validation.js.map