this repo has no description
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