@ix-xs/node-comfort 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +369 -59
- package/src/Func.js +1 -1
- package/src/Http.js +266 -75
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +3 -2
- package/src/Obj.js +225 -36
- package/src/SQLite.js +155 -98
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +124 -23
- package/src/Time.js +98 -24
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +30 -23
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +1 -1
- package/types/src/Http.d.ts +45 -8
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +105 -13
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +43 -10
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +12 -7
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- package/types/src/internal/terminal.d.ts.map +1 -0
package/src/Schema.js
CHANGED
|
@@ -70,7 +70,30 @@ const { UNSAFE_KEYS } = require("./internal/shared.js");
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Options for `parse()`, `safeParse()` and `is()`.
|
|
74
|
+
* @typedef {object} ParseOptions
|
|
75
|
+
* @property {number} [maxDepth] How many levels of objects and arrays a value may have. A value nested deeper is refused with a `"too_deep"` issue instead of running out of stack. Defaults to `1000`.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The state of a parse. `abortEarly` is set by `is()` and while a union
|
|
80
|
+
* tries its options: containers then stop at the first issue, and set
|
|
81
|
+
* `aborted` when they do. `memo` keeps what unions found on objects for
|
|
82
|
+
* the rest of the parse.
|
|
83
|
+
* @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number>, maxDepth: number, abortEarly: boolean, aborted: boolean, memo: { unions: WeakMap<object, Map<Schema<any>, _UnionMemo>> | undefined } | undefined }} ParseContext
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* How an option of a union failed when tried, with paths relative to the
|
|
88
|
+
* union. `aborted` says the issues are only the first ones.
|
|
89
|
+
* @typedef {{ issues: import("./errors").ValidationIssue[], aborted: boolean }} _Attempt
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What a union found on a value: how each option failed when tried, and
|
|
94
|
+
* what it reported while another union tried its options (`fast`) or for
|
|
95
|
+
* the final report (`full`).
|
|
96
|
+
* @typedef {{ tried: _Attempt[], fast: _Attempt | undefined, full: _Attempt | undefined }} _UnionMemo
|
|
74
97
|
*/
|
|
75
98
|
|
|
76
99
|
/**
|
|
@@ -93,6 +116,56 @@ const INVALID = Symbol("invalid");
|
|
|
93
116
|
const _TRUE = ["true", "1", "yes", "on", "y"];
|
|
94
117
|
const _FALSE = ["false", "0", "no", "off", "n", ""];
|
|
95
118
|
const _DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
119
|
+
const _MAX_DEPTH = 1000;
|
|
120
|
+
// What a URL can't hold as written. new URL() would encode it, but the string returned is the raw one.
|
|
121
|
+
const _URL_UNSAFE = /[\u0000- \u007f<>"`\\]/;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A fresh context for a parse.
|
|
125
|
+
* @param {ParseOptions | undefined} options
|
|
126
|
+
* @returns {ParseContext}
|
|
127
|
+
*/
|
|
128
|
+
const _context = (options) => ({ issues: [], path: [], maxDepth: options?.maxDepth ?? _MAX_DEPTH, abortEarly: false, aborted: false, memo: undefined });
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A value for a message: JSON, except bigints (`5n`) and what JSON can't
|
|
132
|
+
* write, like a cycle.
|
|
133
|
+
* @param {unknown} value
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
const _show = (value) => {
|
|
137
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.stringify(value) ?? String(value);
|
|
140
|
+
} catch {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? `${v}n` : v));
|
|
143
|
+
} catch {
|
|
144
|
+
return _typeName(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** @param {unknown} error */
|
|
150
|
+
const _isStackOverflow = (error) => error instanceof RangeError && /call stack/i.test(error.message);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Runs a schema from the top. A value nested so deep that the stack runs
|
|
154
|
+
* out becomes a `"too_deep"` issue instead of an exception.
|
|
155
|
+
* @param {Schema<any>} schema
|
|
156
|
+
* @param {unknown} value
|
|
157
|
+
* @param {ParseContext} ctx
|
|
158
|
+
* @returns {unknown}
|
|
159
|
+
*/
|
|
160
|
+
const _runRoot = (schema, value, ctx) => {
|
|
161
|
+
try {
|
|
162
|
+
return /** @type {any} */ (schema)._run(value, ctx);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!_isStackOverflow(error)) throw error;
|
|
165
|
+
ctx.issues.push({ path: [...ctx.path], code: "too_deep", message: "Nested too deeply" });
|
|
166
|
+
return INVALID;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
96
169
|
|
|
97
170
|
/**
|
|
98
171
|
* What a failed `safeParse()` returns. The error is only built if it's
|
|
@@ -150,6 +223,47 @@ const _typeIssue = (ctx, expected, value, message) => {
|
|
|
150
223
|
_issue(ctx, "invalid_type", message ? _message(message, value) : value === undefined ? "Required" : `Expected ${expected}, received ${received}`, { expected, received });
|
|
151
224
|
};
|
|
152
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Refuses a container nested deeper than allowed. The path holds one key
|
|
228
|
+
* per level, so its length is the depth.
|
|
229
|
+
* @param {ParseContext} ctx
|
|
230
|
+
* @returns {boolean} `true` when the issue was added.
|
|
231
|
+
*/
|
|
232
|
+
const _tooDeep = (ctx) => {
|
|
233
|
+
if (ctx.path.length < ctx.maxDepth) return false;
|
|
234
|
+
_issue(ctx, "too_deep", `Nested more than ${ctx.maxDepth} levels deep`, { maximum: ctx.maxDepth });
|
|
235
|
+
return true;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Issues with their path made relative to a depth.
|
|
240
|
+
* @param {import("./errors").ValidationIssue[]} issues
|
|
241
|
+
* @param {number} depth
|
|
242
|
+
* @returns {import("./errors").ValidationIssue[]}
|
|
243
|
+
*/
|
|
244
|
+
const _from = (issues, depth) => (depth === 0 ? issues : issues.map((issue) => ({ ...issue, path: issue.path.slice(depth) })));
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Relative issues placed under a path.
|
|
248
|
+
* @param {import("./errors").ValidationIssue[]} issues
|
|
249
|
+
* @param {Array<string | number>} path
|
|
250
|
+
* @returns {import("./errors").ValidationIssue[]}
|
|
251
|
+
*/
|
|
252
|
+
const _at = (issues, path) => issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] }));
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Ends a container early while a union tries an option: one issue is
|
|
256
|
+
* enough to know the option doesn't fit.
|
|
257
|
+
* @param {ParseContext} ctx
|
|
258
|
+
* @param {number} before
|
|
259
|
+
* @returns {boolean}
|
|
260
|
+
*/
|
|
261
|
+
const _stop = (ctx, before) => {
|
|
262
|
+
if (!ctx.abortEarly || ctx.issues.length === before) return false;
|
|
263
|
+
ctx.aborted = true;
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
|
|
153
267
|
/**
|
|
154
268
|
* Runs a nested schema.
|
|
155
269
|
* @param {Schema<any>} schema
|
|
@@ -159,6 +273,18 @@ const _typeIssue = (ctx, expected, value, message) => {
|
|
|
159
273
|
*/
|
|
160
274
|
const _runChild = (schema, value, ctx) => /** @type {any} */ (schema)._run(value, ctx);
|
|
161
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Reads a key of an object. What every object inherits, like toString,
|
|
278
|
+
* is not data.
|
|
279
|
+
* @param {Record<string, unknown>} input
|
|
280
|
+
* @param {string} key
|
|
281
|
+
* @returns {unknown}
|
|
282
|
+
*/
|
|
283
|
+
const _read = (input, key) => {
|
|
284
|
+
const raw = input[key];
|
|
285
|
+
return typeof raw === "function" && !Object.hasOwn(input, key) ? undefined : raw;
|
|
286
|
+
};
|
|
287
|
+
|
|
162
288
|
/**
|
|
163
289
|
* The base of every schema.
|
|
164
290
|
*
|
|
@@ -232,7 +358,11 @@ class Schema {
|
|
|
232
358
|
if (ctx.issues.length === before) {
|
|
233
359
|
for (const step of this._steps) {
|
|
234
360
|
if (step.kind === "check") {
|
|
235
|
-
if (!step.test(out))
|
|
361
|
+
if (!step.test(out)) {
|
|
362
|
+
_issue(ctx, step.code, _message(step.message, out), step.details);
|
|
363
|
+
// A value already too long is refused: the checks after, a regex above all, could be very slow on it.
|
|
364
|
+
if ((typeof out === "string" || Array.isArray(out)) && (step.code === "too_big" || (step.code === "invalid_length" && out.length > Number(step.details?.exact)))) break;
|
|
365
|
+
}
|
|
236
366
|
} else if (ctx.issues.length > before) {
|
|
237
367
|
// What follows needs a value that passed everything before it.
|
|
238
368
|
if (step.kind !== "refine") break;
|
|
@@ -267,12 +397,13 @@ class Schema {
|
|
|
267
397
|
* const port = s.coerce.number().int().min(1).max(65535).parse(process.env.PORT);
|
|
268
398
|
*
|
|
269
399
|
* @param {unknown} value
|
|
400
|
+
* @param {ParseOptions} [options]
|
|
270
401
|
* @returns {T}
|
|
271
402
|
* @throws {ValidationError} Listing every issue with its path.
|
|
272
403
|
*/
|
|
273
|
-
parse(value) {
|
|
274
|
-
const ctx =
|
|
275
|
-
const out = this
|
|
404
|
+
parse(value, options) {
|
|
405
|
+
const ctx = _context(options);
|
|
406
|
+
const out = _runRoot(this, value, ctx);
|
|
276
407
|
if (ctx.issues.length) throw new ValidationError(ctx.issues);
|
|
277
408
|
return /** @type {T} */ (out);
|
|
278
409
|
}
|
|
@@ -286,12 +417,12 @@ class Schema {
|
|
|
286
417
|
* saveUser(result.data);
|
|
287
418
|
*
|
|
288
419
|
* @param {unknown} value
|
|
420
|
+
* @param {ParseOptions} [options]
|
|
289
421
|
* @returns {SafeParseResult<T>}
|
|
290
422
|
*/
|
|
291
|
-
safeParse(value) {
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const out = this._run(value, ctx);
|
|
423
|
+
safeParse(value, options) {
|
|
424
|
+
const ctx = _context(options);
|
|
425
|
+
const out = _runRoot(this, value, ctx);
|
|
295
426
|
if (ctx.issues.length) return _failure(ctx.issues);
|
|
296
427
|
return { success: true, data: /** @type {T} */ (out) };
|
|
297
428
|
}
|
|
@@ -303,12 +434,14 @@ class Schema {
|
|
|
303
434
|
* if (Email.is(input)) sendMail(input); // input is a string here
|
|
304
435
|
*
|
|
305
436
|
* @param {unknown} value
|
|
437
|
+
* @param {ParseOptions} [options]
|
|
306
438
|
* @returns {value is T}
|
|
307
439
|
*/
|
|
308
|
-
is(value) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
440
|
+
is(value, options) {
|
|
441
|
+
const ctx = _context(options);
|
|
442
|
+
// No report to build: the first issue answers.
|
|
443
|
+
ctx.abortEarly = true;
|
|
444
|
+
_runRoot(this, value, ctx);
|
|
312
445
|
return ctx.issues.length === 0;
|
|
313
446
|
}
|
|
314
447
|
|
|
@@ -544,7 +677,14 @@ class StringSchema extends Schema {
|
|
|
544
677
|
*/
|
|
545
678
|
_check(value, ctx) {
|
|
546
679
|
let v = value;
|
|
547
|
-
if (this._coerce && v !== undefined && v !== null && typeof v !== "string")
|
|
680
|
+
if (this._coerce && v !== undefined && v !== null && typeof v !== "string") {
|
|
681
|
+
try {
|
|
682
|
+
v = String(v);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
// An array nested too deep to join, or an object without toString().
|
|
685
|
+
if (_isStackOverflow(error)) return _issue(ctx, "too_deep", "Nested too deeply");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
548
688
|
if (typeof v !== "string") return _typeIssue(ctx, "string", value, this._typeMessage);
|
|
549
689
|
return v;
|
|
550
690
|
}
|
|
@@ -612,14 +752,16 @@ class StringSchema extends Schema {
|
|
|
612
752
|
}
|
|
613
753
|
|
|
614
754
|
/**
|
|
615
|
-
* An absolute URL.
|
|
755
|
+
* An absolute URL. The string is returned as written, so spaces, control
|
|
756
|
+
* characters, double quotes, backticks, `<`, `>` and backslashes are
|
|
757
|
+
* refused: a URL that needs them must come percent-encoded.
|
|
616
758
|
* @param {Message | { message?: Message, protocols?: string[] }} [options] A message, or allowed protocols (http and https by default).
|
|
617
759
|
* @returns {this}
|
|
618
760
|
*/
|
|
619
761
|
url(options = "Invalid URL") {
|
|
620
762
|
const opts = typeof options === "object" ? options : { message: options };
|
|
621
763
|
const { isURL } = require("./Checker.js");
|
|
622
|
-
return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
|
|
764
|
+
return this._rule((v) => !_URL_UNSAFE.test(v) && isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
|
|
623
765
|
}
|
|
624
766
|
|
|
625
767
|
/**
|
|
@@ -1104,9 +1246,9 @@ class LiteralSchema extends Schema {
|
|
|
1104
1246
|
_check(value, ctx) {
|
|
1105
1247
|
// A Set treats 0 and -0 as the same value: tell them apart, as before.
|
|
1106
1248
|
if (this._set.has(value) && (value !== 0 || this._values.some((v) => Object.is(v, value)))) return value;
|
|
1107
|
-
const list = this._values.map(
|
|
1249
|
+
const list = this._values.map(_show).join(" | ");
|
|
1108
1250
|
const code = this._values.length > 1 ? "invalid_enum_value" : "invalid_literal";
|
|
1109
|
-
return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${
|
|
1251
|
+
return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${_show(value)}`, { expected: list, received: _typeName(value) });
|
|
1110
1252
|
}
|
|
1111
1253
|
|
|
1112
1254
|
/**
|
|
@@ -1183,13 +1325,16 @@ class ArraySchema extends Schema {
|
|
|
1183
1325
|
*/
|
|
1184
1326
|
_check(value, ctx) {
|
|
1185
1327
|
if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
|
|
1328
|
+
if (_tooDeep(ctx)) return INVALID;
|
|
1186
1329
|
const item = this._item;
|
|
1187
1330
|
const path = ctx.path;
|
|
1331
|
+
const before = ctx.issues.length;
|
|
1188
1332
|
const out = new Array(value.length);
|
|
1189
1333
|
for (let i = 0; i < value.length; i++) {
|
|
1190
1334
|
path.push(i);
|
|
1191
1335
|
out[i] = _runChild(item, value[i], ctx);
|
|
1192
1336
|
path.pop();
|
|
1337
|
+
if (_stop(ctx, before)) return INVALID;
|
|
1193
1338
|
}
|
|
1194
1339
|
return out;
|
|
1195
1340
|
}
|
|
@@ -1295,6 +1440,10 @@ class ObjectSchema extends Schema {
|
|
|
1295
1440
|
this._keys = Object.keys(shape).filter((key) => key !== "__proto__");
|
|
1296
1441
|
/** @protected @type {Array<Schema<any>>} */
|
|
1297
1442
|
this._schemas = this._keys.map((key) => shape[key]);
|
|
1443
|
+
// Keys holding a literal or an enum tell the options of a union apart: they are checked first there.
|
|
1444
|
+
/** @protected @type {number[]} */
|
|
1445
|
+
this._literals = [];
|
|
1446
|
+
for (let i = 0; i < this._schemas.length; i++) if (this._schemas[i] instanceof LiteralSchema) this._literals.push(i);
|
|
1298
1447
|
}
|
|
1299
1448
|
|
|
1300
1449
|
/**
|
|
@@ -1306,20 +1455,35 @@ class ObjectSchema extends Schema {
|
|
|
1306
1455
|
*/
|
|
1307
1456
|
_check(value, ctx) {
|
|
1308
1457
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1458
|
+
if (_tooDeep(ctx)) return INVALID;
|
|
1309
1459
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1310
1460
|
const keys = this._keys;
|
|
1311
1461
|
const schemas = this._schemas;
|
|
1312
1462
|
const path = ctx.path;
|
|
1463
|
+
const before = ctx.issues.length;
|
|
1464
|
+
/** @type {unknown[] | undefined} */
|
|
1465
|
+
let early;
|
|
1466
|
+
if (ctx.abortEarly && this._literals.length) {
|
|
1467
|
+
early = new Array(keys.length);
|
|
1468
|
+
for (const i of this._literals) {
|
|
1469
|
+
path.push(keys[i]);
|
|
1470
|
+
early[i] = _runChild(schemas[i], _read(input, keys[i]), ctx);
|
|
1471
|
+
path.pop();
|
|
1472
|
+
if (_stop(ctx, before)) return INVALID;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1313
1475
|
/** @type {Record<string, unknown>} */
|
|
1314
1476
|
const out = {};
|
|
1315
1477
|
for (let i = 0; i < keys.length; i++) {
|
|
1316
1478
|
const key = keys[i];
|
|
1317
|
-
let
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1479
|
+
let result;
|
|
1480
|
+
if (early !== undefined && i in early) result = early[i];
|
|
1481
|
+
else {
|
|
1482
|
+
path.push(key);
|
|
1483
|
+
result = _runChild(schemas[i], _read(input, key), ctx);
|
|
1484
|
+
path.pop();
|
|
1485
|
+
if (_stop(ctx, before)) return INVALID;
|
|
1486
|
+
}
|
|
1323
1487
|
if (result !== undefined || Object.hasOwn(input, key)) out[key] = result;
|
|
1324
1488
|
}
|
|
1325
1489
|
if (this._unknown === "strip") return out;
|
|
@@ -1510,24 +1674,95 @@ class UnionSchema extends Schema {
|
|
|
1510
1674
|
* @protected
|
|
1511
1675
|
*/
|
|
1512
1676
|
_check(value, ctx) {
|
|
1513
|
-
|
|
1514
|
-
const
|
|
1515
|
-
for
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1677
|
+
const options = this._options;
|
|
1678
|
+
const root = ctx.path.length;
|
|
1679
|
+
// What a union found on an object is remembered for the rest of the
|
|
1680
|
+
// parse, with paths relative to the union: the options of an outer
|
|
1681
|
+
// union often lead to the same nested value.
|
|
1682
|
+
const isObject = value !== null && typeof value === "object";
|
|
1683
|
+
const outermost = ctx.memo === undefined;
|
|
1684
|
+
/** @type {_UnionMemo | undefined} */
|
|
1685
|
+
let memo;
|
|
1686
|
+
if (isObject) {
|
|
1687
|
+
memo = (ctx.memo ??= { unions: undefined }).unions?.get(value)?.get(this);
|
|
1688
|
+
const saved = ctx.abortEarly ? memo?.fast : memo?.full;
|
|
1689
|
+
if (saved) {
|
|
1690
|
+
ctx.issues.push(..._at(saved.issues, ctx.path));
|
|
1691
|
+
if (saved.aborted) ctx.aborted = true;
|
|
1692
|
+
return INVALID;
|
|
1693
|
+
}
|
|
1520
1694
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1695
|
+
// Each option is first tried in a context that stops at its first
|
|
1696
|
+
// issue. Trying every option in full made each level of a recursive
|
|
1697
|
+
// union cost twice the one below, so a small input could take seconds.
|
|
1698
|
+
/** @type {_Attempt[]} */
|
|
1699
|
+
let tried;
|
|
1700
|
+
if (memo) tried = memo.tried.map((attempt) => ({ issues: _at(attempt.issues, ctx.path), aborted: attempt.aborted }));
|
|
1701
|
+
else {
|
|
1702
|
+
tried = [];
|
|
1703
|
+
for (let i = 0; i < options.length; i++) {
|
|
1704
|
+
/** @type {ParseContext} */
|
|
1705
|
+
const attempt = { issues: [], path: ctx.path, maxDepth: ctx.maxDepth, abortEarly: true, aborted: false, memo: ctx.memo };
|
|
1706
|
+
const out = _runChild(options[i], value, attempt);
|
|
1707
|
+
if (!attempt.issues.length) return out;
|
|
1708
|
+
tried.push(attempt);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/** @type {import("./errors").ValidationIssue[]} */
|
|
1712
|
+
let issues;
|
|
1713
|
+
let aborted = false;
|
|
1714
|
+
if (ctx.abortEarly) {
|
|
1715
|
+
// Another union is trying its options, or is() only asks yes or no: one issue is enough, unless every one is already known.
|
|
1716
|
+
aborted = tried.some((attempt) => attempt.aborted);
|
|
1717
|
+
issues = aborted ? [tried[0].issues[0]] : this._report(tried.map((attempt) => attempt.issues), value, ctx.path);
|
|
1718
|
+
} else {
|
|
1719
|
+
// The options that stopped early run again, in full, for the report.
|
|
1720
|
+
/** @type {import("./errors").ValidationIssue[][]} */
|
|
1721
|
+
const full = [];
|
|
1722
|
+
for (let i = 0; i < tried.length; i++) {
|
|
1723
|
+
if (!tried[i].aborted) {
|
|
1724
|
+
full.push(tried[i].issues);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
/** @type {ParseContext} */
|
|
1728
|
+
const again = { issues: [], path: ctx.path, maxDepth: ctx.maxDepth, abortEarly: false, aborted: false, memo: ctx.memo };
|
|
1729
|
+
const out = _runChild(options[i], value, again);
|
|
1730
|
+
if (!again.issues.length) return out;
|
|
1731
|
+
full.push(again.issues);
|
|
1732
|
+
}
|
|
1733
|
+
issues = this._report(full, value, ctx.path);
|
|
1734
|
+
}
|
|
1735
|
+
// Only a union inside another one can meet the same value again.
|
|
1736
|
+
if (isObject && !outermost && ctx.memo) {
|
|
1737
|
+
const unions = (ctx.memo.unions ??= new WeakMap());
|
|
1738
|
+
let known = unions.get(value);
|
|
1739
|
+
if (!known) unions.set(value, (known = new Map()));
|
|
1740
|
+
memo ??= { tried: tried.map((attempt) => ({ issues: _from(attempt.issues, root), aborted: attempt.aborted })), fast: undefined, full: undefined };
|
|
1741
|
+
known.set(this, memo);
|
|
1742
|
+
memo[ctx.abortEarly ? "fast" : "full"] = { issues: _from(issues, root), aborted };
|
|
1525
1743
|
}
|
|
1526
|
-
|
|
1527
|
-
ctx.
|
|
1744
|
+
ctx.issues.push(...issues);
|
|
1745
|
+
if (aborted) ctx.aborted = true;
|
|
1528
1746
|
return INVALID;
|
|
1529
1747
|
}
|
|
1530
1748
|
|
|
1749
|
+
/**
|
|
1750
|
+
* The issues to report once every option failed: a single one when no
|
|
1751
|
+
* option had the right type, or those of the option with the fewest.
|
|
1752
|
+
* @param {import("./errors").ValidationIssue[][]} failures
|
|
1753
|
+
* @param {unknown} value
|
|
1754
|
+
* @param {Array<string | number>} path Where the union is.
|
|
1755
|
+
* @returns {import("./errors").ValidationIssue[]}
|
|
1756
|
+
* @protected
|
|
1757
|
+
*/
|
|
1758
|
+
_report(failures, value, path) {
|
|
1759
|
+
if (failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === path.length)) {
|
|
1760
|
+
const expected = failures.map((issues) => issues[0].expected).join(" | ");
|
|
1761
|
+
return [{ path: [...path], code: "invalid_union", message: `Expected ${expected}, received ${_typeName(value)}`, expected, received: _typeName(value) }];
|
|
1762
|
+
}
|
|
1763
|
+
return failures.reduce((a, b) => (b.length < a.length ? b : a));
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1531
1766
|
/**
|
|
1532
1767
|
* @param {_JSONContext} ctx
|
|
1533
1768
|
* @returns {JSONSchema}
|
|
@@ -1562,7 +1797,7 @@ class DiscriminatedUnionSchema extends Schema {
|
|
|
1562
1797
|
const values = /** @type {any} */ (option)?._shape?.[key]?._values;
|
|
1563
1798
|
if (!Array.isArray(values)) throw new TypeError(`discriminatedUnion(): every option must be an object with a literal "${key}"`);
|
|
1564
1799
|
for (const value of values) {
|
|
1565
|
-
if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${
|
|
1800
|
+
if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${_show(value)}`);
|
|
1566
1801
|
this._byValue.set(value, option);
|
|
1567
1802
|
}
|
|
1568
1803
|
}
|
|
@@ -1580,9 +1815,9 @@ class DiscriminatedUnionSchema extends Schema {
|
|
|
1580
1815
|
const tag = /** @type {Record<string, unknown>} */ (value)[this._key];
|
|
1581
1816
|
const option = this._byValue.get(tag);
|
|
1582
1817
|
if (option) return _runChild(option, value, ctx);
|
|
1583
|
-
const expected = [...this._byValue.keys()].map(
|
|
1818
|
+
const expected = [...this._byValue.keys()].map(_show).join(" | ");
|
|
1584
1819
|
ctx.path.push(this._key);
|
|
1585
|
-
_issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${
|
|
1820
|
+
_issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${_show(tag)}`, { expected, received: _typeName(tag) });
|
|
1586
1821
|
ctx.path.pop();
|
|
1587
1822
|
return INVALID;
|
|
1588
1823
|
}
|
|
@@ -1624,12 +1859,15 @@ class TupleSchema extends Schema {
|
|
|
1624
1859
|
if (value.length !== this._items.length) {
|
|
1625
1860
|
return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
|
|
1626
1861
|
}
|
|
1862
|
+
if (_tooDeep(ctx)) return INVALID;
|
|
1627
1863
|
const path = ctx.path;
|
|
1864
|
+
const before = ctx.issues.length;
|
|
1628
1865
|
const out = new Array(this._items.length);
|
|
1629
1866
|
for (let i = 0; i < out.length; i++) {
|
|
1630
1867
|
path.push(i);
|
|
1631
1868
|
out[i] = _runChild(this._items[i], value[i], ctx);
|
|
1632
1869
|
path.pop();
|
|
1870
|
+
if (_stop(ctx, before)) return INVALID;
|
|
1633
1871
|
}
|
|
1634
1872
|
return out;
|
|
1635
1873
|
}
|
|
@@ -1673,8 +1911,10 @@ class RecordSchema extends Schema {
|
|
|
1673
1911
|
*/
|
|
1674
1912
|
_check(value, ctx) {
|
|
1675
1913
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1914
|
+
if (_tooDeep(ctx)) return INVALID;
|
|
1676
1915
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1677
1916
|
const path = ctx.path;
|
|
1917
|
+
const before = ctx.issues.length;
|
|
1678
1918
|
/** @type {Record<string, unknown>} */
|
|
1679
1919
|
const out = {};
|
|
1680
1920
|
for (const key of Object.keys(input)) {
|
|
@@ -1683,6 +1923,7 @@ class RecordSchema extends Schema {
|
|
|
1683
1923
|
const k = this._keys ? _runChild(this._keys, key, ctx) : key;
|
|
1684
1924
|
const v = _runChild(this._values, input[key], ctx);
|
|
1685
1925
|
path.pop();
|
|
1926
|
+
if (_stop(ctx, before)) return INVALID;
|
|
1686
1927
|
if (typeof k === "string" && !UNSAFE_KEYS.has(k)) out[k] = v;
|
|
1687
1928
|
}
|
|
1688
1929
|
return out;
|