summaryrefslogtreecommitdiff
path: root/node_modules/@jet/environment/json/validation.js
blob: b338c4ef2fc95ec897d1f2ec5862819edf1c6565 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"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