validation.js
  1  "use strict";
  2  Object.defineProperty(exports, "__esModule", { value: true });
  3  exports.unexpectedNull = exports.catchingContext = exports.context = exports.recordValidationIncidents = exports.endContext = exports.getContextNames = exports.beginContext = exports.messageForRecoveryAction = exports.isValidatable = exports.unexpectedType = exports.extendedTypeof = void 0;
  4  const 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   */
 16  function 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  }
 27  exports.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   */
 35  function 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  }
 50  exports.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   */
 58  function 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  }
 69  exports.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   */
 75  function 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  }
 87  exports.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   */
 99  const 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   */
115  function beginContext(name) {
116      contextState.nameStack.push(name);
117  }
118  exports.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   */
123  function getContextNames() {
124      if (contextState.nameStack.length === 0) {
125          return ["<empty stack>"];
126      }
127      return contextState.nameStack.slice(0);
128  }
129  exports.getContextNames = getContextNames;
130  /**
131   * Ends the current validation context
132   */
133  function endContext() {
134      if (contextState.nameStack.length === 0) {
135          console.warn("endContext() called without active validation context, ignoring");
136      }
137      contextState.nameStack.pop();
138  }
139  exports.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   */
149  function recordValidationIncidents(possibleValidatable) {
150      if (isValidatable(possibleValidatable)) {
151          possibleValidatable.$incidents = contextState.incidents;
152      }
153      contextState.incidents = [];
154      contextState.nameStack = [];
155      contextState.suppressedIncidentPaths = [];
156  }
157  exports.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   */
168  function 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  }
195  exports.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   */
204  function 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  }
217  exports.catchingContext = catchingContext;
218  /**
219   * Track an incident within the current validation context.
220   * @param incident - An incident object describing the problem.
221   */
222  function 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   */
236  function 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  }
248  exports.unexpectedNull = unexpectedNull;
249  // endregion
250  //# sourceMappingURL=validation.js.map