Files
apps.apple.com/node_modules/@jet/environment/json/validation.js
2025-11-04 05:03:50 +08:00

250 lines
8.9 KiB
JavaScript

"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 : "<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 ((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:
// <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 ((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 <Result> 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 : "<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