"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 = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : ""; 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 ((0, 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: // 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 [""]; } 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 The resulting type */ function context(name, producer, suppressingPath) { let suppressingName = null; if ((0, optional_1.isSome)(suppressingPath) && suppressingPath.length > 0) { suppressingName = name; contextState.suppressedIncidentPaths.push(suppressingPath); } let result; 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 The resulting type */ function catchingContext(name, producer, caught) { let result = null; try { result = context(name, producer); } catch (e) { result = null; if ((0, 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 = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : ""; 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