this repo has no description
at main 9.1 kB view raw
1"use strict"; 2Object.defineProperty(exports, "__esModule", { value: true }); 3exports.unexpectedNull = exports.catchingContext = exports.context = exports.recordValidationIncidents = exports.endContext = exports.getContextNames = exports.beginContext = exports.messageForRecoveryAction = exports.isValidatable = exports.unexpectedType = exports.extendedTypeof = void 0; 4const optional_1 = require("../types/optional"); 5/** 6 * Returns a string containing the type of a given value. 7 * This function augments the built in `typeof` operator 8 * to return sensible values for arrays and null values. 9 * 10 * @privateRemarks 11 * This function is exported for testing. 12 * 13 * @param value - The value to find the type of. 14 * @returns A string containing the type of `value`. 15 */ 16function extendedTypeof(value) { 17 if (Array.isArray(value)) { 18 return "array"; 19 } 20 else if (value === null) { 21 return "null"; 22 } 23 else { 24 return typeof value; 25 } 26} 27exports.extendedTypeof = extendedTypeof; 28/** 29 * Reports a non-fatal validation failure, logging a message to the console. 30 * @param recovery - The recovery action taken when the bad type was found. 31 * @param expected - The expected type of the value. 32 * @param actual - The actual value. 33 * @param pathString - A string containing the path to the value on the object which failed type validation. 34 */ 35function unexpectedType(recovery, expected, actual, pathString) { 36 const actualType = extendedTypeof(actual); 37 const prettyPath = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : "<this>"; 38 trackIncident({ 39 type: "badType", 40 expected: expected, 41 // Our test assertions are matching the string interpolation of ${actual} value. 42 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 43 actual: `${actualType} (${actual})`, 44 objectPath: prettyPath, 45 contextNames: getContextNames(), 46 recoveryAction: recovery, 47 stack: new Error().stack, 48 }); 49} 50exports.unexpectedType = unexpectedType; 51// endregion 52/** 53 * Determines if a given object conforms to the Validatable interface 54 * @param possibleValidatable - An object that might be considered validatable 55 * 56 * @returns `true` if it is an instance of Validatable, `false` if not 57 */ 58function isValidatable(possibleValidatable) { 59 if ((0, optional_1.isNothing)(possibleValidatable)) { 60 return false; 61 } 62 // MAINTAINER'S NOTE: We must check for either the existence of a pre-existing incidents 63 // property *or* the ability to add one. Failure to do so will cause 64 // problems for clients that either a) use interfaces to define their 65 // view models; or b) return collections from their service routes. 66 return (Object.prototype.hasOwnProperty.call(possibleValidatable, "$incidents") || 67 Object.isExtensible(possibleValidatable)); 68} 69exports.isValidatable = isValidatable; 70/** 71 * Returns a developer-readable diagnostic message for a given recovery action. 72 * @param action - The recovery action to get the message for. 73 * @returns The message for `action`. 74 */ 75function messageForRecoveryAction(action) { 76 switch (action) { 77 case "coercedValue": 78 return "Coerced format"; 79 case "defaultValue": 80 return "Default value used"; 81 case "ignoredValue": 82 return "Ignored value"; 83 default: 84 return "Unknown"; 85 } 86} 87exports.messageForRecoveryAction = messageForRecoveryAction; 88// region Contexts 89/** 90 * Shared validation context "stack". 91 * 92 * Because validation incidents propagate up the context stack, 93 * the representation used here is optimized for memory usage. 94 * A more literal representation of this would be a singly linked 95 * list describing a basic stack, but that will produce a large 96 * amount of unnecessary garbage and require copying `incidents` 97 * arrays backwards. 98 */ 99const contextState = { 100 /// The names of each validation context on the stack. 101 nameStack: Array(), 102 /// All incidents reported so far. Cleared when the 103 /// context stack is emptied. 104 incidents: Array(), 105 // TODO: Removal of this is being tracked here: 106 // <rdar://problem/35015460> Intro Pricing: Un-suppress missing parent 'offers' error when server address missing key 107 /// The paths for incidents we wish to forgo tracking. 108 suppressedIncidentPaths: Array(), 109}; 110/** 111 * Begin a new validation context with a given name, 112 * pushing it onto the validation context stack. 113 * @param name - The name for the validation context. 114 */ 115function beginContext(name) { 116 contextState.nameStack.push(name); 117} 118exports.beginContext = beginContext; 119/** 120 * Traverses the validation context stack and collects all of the context names. 121 * @returns The names of all validation contexts on the stack, from oldest to newest. 122 */ 123function getContextNames() { 124 if (contextState.nameStack.length === 0) { 125 return ["<empty stack>"]; 126 } 127 return contextState.nameStack.slice(0); 128} 129exports.getContextNames = getContextNames; 130/** 131 * Ends the current validation context 132 */ 133function endContext() { 134 if (contextState.nameStack.length === 0) { 135 console.warn("endContext() called without active validation context, ignoring"); 136 } 137 contextState.nameStack.pop(); 138} 139exports.endContext = endContext; 140/** 141 * Records validation incidents back into an object that implements Validatable. 142 * 143 * Note: This method has a side-effect that the incident queue and name stack are cleared 144 * to prepare for the next thread's invocation. 145 * 146 * @param possibleValidatable - An object that may conform to Validatable, onto which we 147 * want to stash our validation incidents 148 */ 149function recordValidationIncidents(possibleValidatable) { 150 if (isValidatable(possibleValidatable)) { 151 possibleValidatable.$incidents = contextState.incidents; 152 } 153 contextState.incidents = []; 154 contextState.nameStack = []; 155 contextState.suppressedIncidentPaths = []; 156} 157exports.recordValidationIncidents = recordValidationIncidents; 158/** 159 * Create a transient validation context, and call a function that will return a value. 160 * 161 * Prefer this function over manually calling begin/endContext, 162 * it is exception safe. 163 * 164 * @param name - The name of the context 165 * @param producer - A function that produces a result 166 * @returns <Result> The resulting type 167 */ 168function context(name, producer, suppressingPath) { 169 let suppressingName = null; 170 if ((0, optional_1.isSome)(suppressingPath) && suppressingPath.length > 0) { 171 suppressingName = name; 172 contextState.suppressedIncidentPaths.push(suppressingPath); 173 } 174 let result; 175 try { 176 beginContext(name); 177 result = producer(); 178 } 179 catch (e) { 180 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 181 if (!e.hasThrown) { 182 unexpectedType("defaultValue", "no exception", e.message); 183 e.hasThrown = true; 184 } 185 throw e; 186 } 187 finally { 188 if (name === suppressingName) { 189 contextState.suppressedIncidentPaths.pop(); 190 } 191 endContext(); 192 } 193 return result; 194} 195exports.context = context; 196/** 197 * Create a transient validation context, that catches errors and returns null 198 * 199 * @param name - The name of the context 200 * @param producer - A function that produces a result 201 * @param caught - An optional handler to provide a value when an error is caught 202 * @returns <Result> The resulting type 203 */ 204function catchingContext(name, producer, caught) { 205 let result = null; 206 try { 207 result = context(name, producer); 208 } 209 catch (e) { 210 result = null; 211 if ((0, optional_1.isSome)(caught)) { 212 result = caught(e); 213 } 214 } 215 return result; 216} 217exports.catchingContext = catchingContext; 218/** 219 * Track an incident within the current validation context. 220 * @param incident - An incident object describing the problem. 221 */ 222function trackIncident(incident) { 223 if (contextState.suppressedIncidentPaths.includes(incident.objectPath)) { 224 return; 225 } 226 contextState.incidents.push(incident); 227} 228// endregion 229// region Nullability 230/** 231 * Reports a non-fatal error indicating a value was unexpectedly null. 232 * @param recovery - The recovery action taken when the null value was found. 233 * @param expected - The expected type of the value. 234 * @param pathString - A string containing the path to the value on the object which was null. 235 */ 236function unexpectedNull(recovery, expected, pathString) { 237 const prettyPath = (0, optional_1.isSome)(pathString) && pathString.length > 0 ? pathString : "<this>"; 238 trackIncident({ 239 type: "nullValue", 240 expected: expected, 241 actual: "null", 242 objectPath: prettyPath, 243 contextNames: getContextNames(), 244 recoveryAction: recovery, 245 stack: new Error().stack, 246 }); 247} 248exports.unexpectedNull = unexpectedNull; 249// endregion 250//# sourceMappingURL=validation.js.map