mirror of
https://github.com/rxliuli/apps.apple.com.git
synced 2025-11-09 22:00:32 +00:00
250 lines
8.9 KiB
JavaScript
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
|