@ix-xs/node-comfort 2.0.0 → 2.1.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 +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Schema.js
CHANGED
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
* });
|
|
16
16
|
*
|
|
17
17
|
* const user = User.parse(req.body); // typed, or a ValidationError listing every issue
|
|
18
|
-
* const result = User.safeParse(input); // { success, data } or { success, error }
|
|
18
|
+
* const result = User.safeParse(input); // { success, data } or { success, error, issues }
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const { ValidationError } = require("./errors.js");
|
|
22
21
|
const { types } = require("node:util");
|
|
22
|
+
const { ValidationError } = require("./errors.js");
|
|
23
|
+
const { UNSAFE_KEYS } = require("./internal/shared.js");
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The type a schema produces.
|
|
@@ -49,9 +50,10 @@ const { types } = require("node:util");
|
|
|
49
50
|
*/
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* What `safeParse()` returns.
|
|
53
|
+
* What `safeParse()` returns. On failure, `issues` is the plain list and
|
|
54
|
+
* `error` the same thing as a `ValidationError`, built when you read it.
|
|
53
55
|
* @template T
|
|
54
|
-
* @typedef {{ success: true, data: T, error?: undefined } | { success: false, data?: undefined, error: ValidationError }} SafeParseResult
|
|
56
|
+
* @typedef {{ success: true, data: T, error?: undefined, issues?: undefined } | { success: false, data?: undefined, error: ValidationError, issues: import("./errors").ValidationIssue[] }} SafeParseResult
|
|
55
57
|
*/
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -72,12 +74,45 @@ const { types } = require("node:util");
|
|
|
72
74
|
*/
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
|
-
* A step in a schema: a check, a transformation or a
|
|
76
|
-
*
|
|
77
|
+
* A step in a schema: a check, a transformation, a refinement or a pipe
|
|
78
|
+
* into another schema.
|
|
79
|
+
* @typedef {{ kind: "check", test: (value: any) => boolean, code: string, message: Message, details?: Record<string, unknown>, json?: Record<string, unknown> } | { kind: "transform", fn: (value: any) => any } | { kind: "refine", test: (value: any) => boolean, options: RefineOptions } | { kind: "pipe", schema: Schema<any> }} Step
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A JSON Schema, as made by `toJSONSchema()`.
|
|
84
|
+
* @typedef {{ [keyword: string]: any }} JSONSchema
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {{ defs: Record<string, JSONSchema>, names: Map<Schema<any>, string> }} _JSONContext
|
|
77
89
|
*/
|
|
78
90
|
|
|
79
91
|
const INVALID = Symbol("invalid");
|
|
80
92
|
|
|
93
|
+
const _TRUE = ["true", "1", "yes", "on", "y"];
|
|
94
|
+
const _FALSE = ["false", "0", "no", "off", "n", ""];
|
|
95
|
+
const _DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* What a failed `safeParse()` returns. The error is only built if it's
|
|
99
|
+
* read: capturing a stack trace costs far more than the validation itself.
|
|
100
|
+
* @param {import("./errors").ValidationIssue[]} issues
|
|
101
|
+
* @returns {{ success: false, error: ValidationError, issues: import("./errors").ValidationIssue[] }}
|
|
102
|
+
*/
|
|
103
|
+
const _failure = (issues) => {
|
|
104
|
+
/** @type {ValidationError | undefined} */
|
|
105
|
+
let error;
|
|
106
|
+
return /** @type {any} */ (Object.defineProperty({ success: false, issues }, "error", { enumerable: true, get: () => (error ??= new ValidationError(issues)) }));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {Schema<any>} schema
|
|
111
|
+
* @param {_JSONContext} ctx
|
|
112
|
+
* @returns {JSONSchema}
|
|
113
|
+
*/
|
|
114
|
+
const _jsonOf = (schema, ctx) => /** @type {any} */ (schema)._toJSON(ctx);
|
|
115
|
+
|
|
81
116
|
/** @param {unknown} value */
|
|
82
117
|
const _typeName = (value) => {
|
|
83
118
|
if (value === null) return "null";
|
|
@@ -188,7 +223,7 @@ class Schema {
|
|
|
188
223
|
let value = input;
|
|
189
224
|
if (value === undefined && this._default) {
|
|
190
225
|
const def = this._default.value;
|
|
191
|
-
value = typeof def === "function" ? def() :
|
|
226
|
+
value = typeof def === "function" ? def() : def !== null && typeof def === "object" ? require("./Obj.js").clone(def) : def;
|
|
192
227
|
}
|
|
193
228
|
if (value === undefined && this._optional) return undefined;
|
|
194
229
|
if (value === null && this._nullable) return null;
|
|
@@ -196,14 +231,18 @@ class Schema {
|
|
|
196
231
|
let out = this._check(value, ctx);
|
|
197
232
|
if (ctx.issues.length === before) {
|
|
198
233
|
for (const step of this._steps) {
|
|
199
|
-
if (step.kind === "
|
|
200
|
-
if (ctx.issues.length > before) break;
|
|
201
|
-
out = step.fn(out);
|
|
202
|
-
} else if (step.kind === "check") {
|
|
234
|
+
if (step.kind === "check") {
|
|
203
235
|
if (!step.test(out)) _issue(ctx, step.code, _message(step.message, out), step.details);
|
|
204
|
-
} else if (ctx.issues.length
|
|
236
|
+
} else if (ctx.issues.length > before) {
|
|
237
|
+
// What follows needs a value that passed everything before it.
|
|
238
|
+
if (step.kind !== "refine") break;
|
|
239
|
+
} else if (step.kind === "transform") {
|
|
240
|
+
out = step.fn(out);
|
|
241
|
+
} else if (step.kind === "pipe") {
|
|
242
|
+
out = _runChild(step.schema, out, ctx);
|
|
243
|
+
} else if (!step.test(out)) {
|
|
205
244
|
const saved = ctx.path;
|
|
206
|
-
ctx.path = [...
|
|
245
|
+
if (step.options.path) ctx.path = [...saved, ...step.options.path];
|
|
207
246
|
_issue(ctx, step.options.code ?? "custom", _message(step.options.message ?? "Invalid value", out));
|
|
208
247
|
ctx.path = saved;
|
|
209
248
|
}
|
|
@@ -250,14 +289,15 @@ class Schema {
|
|
|
250
289
|
* @returns {SafeParseResult<T>}
|
|
251
290
|
*/
|
|
252
291
|
safeParse(value) {
|
|
292
|
+
/** @type {ParseContext} */
|
|
253
293
|
const ctx = { issues: [], path: [] };
|
|
254
294
|
const out = this._run(value, ctx);
|
|
255
|
-
if (ctx.issues.length) return
|
|
295
|
+
if (ctx.issues.length) return _failure(ctx.issues);
|
|
256
296
|
return { success: true, data: /** @type {T} */ (out) };
|
|
257
297
|
}
|
|
258
298
|
|
|
259
299
|
/**
|
|
260
|
-
* Is the value valid? Works as a type guard
|
|
300
|
+
* Is the value valid? Works as a type guard, and builds no error.
|
|
261
301
|
*
|
|
262
302
|
* @example
|
|
263
303
|
* if (Email.is(input)) sendMail(input); // input is a string here
|
|
@@ -266,7 +306,10 @@ class Schema {
|
|
|
266
306
|
* @returns {value is T}
|
|
267
307
|
*/
|
|
268
308
|
is(value) {
|
|
269
|
-
|
|
309
|
+
/** @type {ParseContext} */
|
|
310
|
+
const ctx = { issues: [], path: [] };
|
|
311
|
+
this._run(value, ctx);
|
|
312
|
+
return ctx.issues.length === 0;
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
/**
|
|
@@ -394,6 +437,76 @@ class Schema {
|
|
|
394
437
|
return array(/** @type {Schema<T>} */ (this));
|
|
395
438
|
}
|
|
396
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Sends the result into another schema. With `transform()` in between,
|
|
442
|
+
* it validates what a conversion produced.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* const Port = s.string().transform(Number).pipe(s.number().int().min(1).max(65535));
|
|
446
|
+
* Port.parse("8080"); // 8080
|
|
447
|
+
* Port.parse("abc"); // ValidationError: Expected number, received NaN
|
|
448
|
+
*
|
|
449
|
+
* @template U
|
|
450
|
+
* @param {Schema<U>} next
|
|
451
|
+
* @returns {Schema<U>}
|
|
452
|
+
*/
|
|
453
|
+
pipe(next) {
|
|
454
|
+
return /** @type {any} */ (this._with({ kind: "pipe", schema: next }));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Describes what this schema accepts as a JSON Schema (draft 2020-12).
|
|
459
|
+
* Use it for an OpenAPI document, a form generator, or the input of a
|
|
460
|
+
* tool you give to a language model. Rules that only exist in code,
|
|
461
|
+
* like `refine()`, `transform()` and what follows a `pipe()`, can't be
|
|
462
|
+
* expressed and are left out.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* s.object({ name: s.string().min(2), age: s.number().int().optional() }).toJSONSchema();
|
|
466
|
+
* // {
|
|
467
|
+
* // type: "object",
|
|
468
|
+
* // properties: { name: { type: "string", minLength: 2 }, age: { type: "integer" } },
|
|
469
|
+
* // required: ["name"],
|
|
470
|
+
* // }
|
|
471
|
+
*
|
|
472
|
+
* @returns {JSONSchema}
|
|
473
|
+
*/
|
|
474
|
+
toJSONSchema() {
|
|
475
|
+
/** @type {_JSONContext} */
|
|
476
|
+
const ctx = { defs: {}, names: new Map() };
|
|
477
|
+
const out = this._toJSON(ctx);
|
|
478
|
+
if (ctx.names.size) out.$defs = ctx.defs;
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The JSON Schema of the type itself. Subclasses override it.
|
|
484
|
+
* @param {_JSONContext} _ctx
|
|
485
|
+
* @returns {JSONSchema}
|
|
486
|
+
* @protected
|
|
487
|
+
*/
|
|
488
|
+
_json(_ctx) {
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The JSON Schema with what every schema can carry: rules, `null`,
|
|
494
|
+
* a description, a default.
|
|
495
|
+
* @param {_JSONContext} ctx
|
|
496
|
+
* @returns {JSONSchema}
|
|
497
|
+
* @protected
|
|
498
|
+
*/
|
|
499
|
+
_toJSON(ctx) {
|
|
500
|
+
let out = this._json(ctx);
|
|
501
|
+
for (const step of this._steps) {
|
|
502
|
+
if (step.kind === "check" && step.json) Object.assign(out, step.json);
|
|
503
|
+
}
|
|
504
|
+
if (this._nullable) out = { anyOf: [out, { type: "null" }] };
|
|
505
|
+
if (this._description !== undefined) out.description = this._description;
|
|
506
|
+
if (this._default && typeof this._default.value !== "function") out.default = this._default.value;
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
397
510
|
/**
|
|
398
511
|
* Attaches a description, available as `.description`. Useful to generate
|
|
399
512
|
* docs or forms.
|
|
@@ -441,11 +554,12 @@ class StringSchema extends Schema {
|
|
|
441
554
|
* @param {string} code
|
|
442
555
|
* @param {Message} message
|
|
443
556
|
* @param {Record<string, unknown>} [details]
|
|
557
|
+
* @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
|
|
444
558
|
* @returns {this}
|
|
445
559
|
* @protected
|
|
446
560
|
*/
|
|
447
|
-
_rule(test, code, message, details) {
|
|
448
|
-
return this._with({ kind: "check", test, code, message, details });
|
|
561
|
+
_rule(test, code, message, details, json) {
|
|
562
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
449
563
|
}
|
|
450
564
|
|
|
451
565
|
/**
|
|
@@ -455,7 +569,7 @@ class StringSchema extends Schema {
|
|
|
455
569
|
* @returns {this}
|
|
456
570
|
*/
|
|
457
571
|
min(length, message = `Must contain at least ${length} character${length > 1 ? "s" : ""}`) {
|
|
458
|
-
return this._rule((v) => v.length >= length, "too_small", message, { minimum: length });
|
|
572
|
+
return this._rule((v) => v.length >= length, "too_small", message, { minimum: length }, { minLength: length });
|
|
459
573
|
}
|
|
460
574
|
|
|
461
575
|
/**
|
|
@@ -465,7 +579,7 @@ class StringSchema extends Schema {
|
|
|
465
579
|
* @returns {this}
|
|
466
580
|
*/
|
|
467
581
|
max(length, message = `Must contain at most ${length} character${length > 1 ? "s" : ""}`) {
|
|
468
|
-
return this._rule((v) => v.length <= length, "too_big", message, { maximum: length });
|
|
582
|
+
return this._rule((v) => v.length <= length, "too_big", message, { maximum: length }, { maxLength: length });
|
|
469
583
|
}
|
|
470
584
|
|
|
471
585
|
/**
|
|
@@ -475,7 +589,7 @@ class StringSchema extends Schema {
|
|
|
475
589
|
* @returns {this}
|
|
476
590
|
*/
|
|
477
591
|
length(length, message = `Must contain exactly ${length} characters`) {
|
|
478
|
-
return this._rule((v) => v.length === length, "invalid_length", message, { exact: length });
|
|
592
|
+
return this._rule((v) => v.length === length, "invalid_length", message, { exact: length }, { minLength: length, maxLength: length });
|
|
479
593
|
}
|
|
480
594
|
|
|
481
595
|
/**
|
|
@@ -484,7 +598,7 @@ class StringSchema extends Schema {
|
|
|
484
598
|
* @returns {this}
|
|
485
599
|
*/
|
|
486
600
|
nonempty(message = "Must not be empty") {
|
|
487
|
-
return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 });
|
|
601
|
+
return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minLength: 1 });
|
|
488
602
|
}
|
|
489
603
|
|
|
490
604
|
/**
|
|
@@ -494,7 +608,7 @@ class StringSchema extends Schema {
|
|
|
494
608
|
*/
|
|
495
609
|
email(message = "Invalid email address") {
|
|
496
610
|
const { isEmail } = require("./Checker.js");
|
|
497
|
-
return this._rule(isEmail, "invalid_string", message, { validation: "email" });
|
|
611
|
+
return this._rule(isEmail, "invalid_string", message, { validation: "email" }, { format: "email" });
|
|
498
612
|
}
|
|
499
613
|
|
|
500
614
|
/**
|
|
@@ -505,7 +619,7 @@ class StringSchema extends Schema {
|
|
|
505
619
|
url(options = "Invalid URL") {
|
|
506
620
|
const opts = typeof options === "object" ? options : { message: options };
|
|
507
621
|
const { isURL } = require("./Checker.js");
|
|
508
|
-
return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" });
|
|
622
|
+
return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
|
|
509
623
|
}
|
|
510
624
|
|
|
511
625
|
/**
|
|
@@ -515,7 +629,7 @@ class StringSchema extends Schema {
|
|
|
515
629
|
*/
|
|
516
630
|
uuid(message = "Invalid UUID") {
|
|
517
631
|
const { isUUID } = require("./Checker.js");
|
|
518
|
-
return this._rule((v) => isUUID(v), "invalid_string", message, { validation: "uuid" });
|
|
632
|
+
return this._rule((v) => isUUID(v), "invalid_string", message, { validation: "uuid" }, { format: "uuid" });
|
|
519
633
|
}
|
|
520
634
|
|
|
521
635
|
/**
|
|
@@ -527,7 +641,7 @@ class StringSchema extends Schema {
|
|
|
527
641
|
ip(versionOrMessage, message = "Invalid IP address") {
|
|
528
642
|
const version = typeof versionOrMessage === "number" ? versionOrMessage : undefined;
|
|
529
643
|
const { isIP } = require("./Checker.js");
|
|
530
|
-
return this._rule((v) => isIP(v, version), "invalid_string", typeof versionOrMessage === "number" ? message : versionOrMessage ?? message, { validation: "ip" });
|
|
644
|
+
return this._rule((v) => isIP(v, version), "invalid_string", typeof versionOrMessage === "number" ? message : versionOrMessage ?? message, { validation: "ip" }, version === 4 ? { format: "ipv4" } : version === 6 ? { format: "ipv6" } : undefined);
|
|
531
645
|
}
|
|
532
646
|
|
|
533
647
|
/**
|
|
@@ -537,7 +651,7 @@ class StringSchema extends Schema {
|
|
|
537
651
|
*/
|
|
538
652
|
datetime(message = "Invalid ISO date") {
|
|
539
653
|
const { isISODate } = require("./Checker.js");
|
|
540
|
-
return this._rule(isISODate, "invalid_string", message, { validation: "datetime" });
|
|
654
|
+
return this._rule(isISODate, "invalid_string", message, { validation: "datetime" }, { format: "date-time" });
|
|
541
655
|
}
|
|
542
656
|
|
|
543
657
|
/**
|
|
@@ -550,7 +664,7 @@ class StringSchema extends Schema {
|
|
|
550
664
|
return this._rule((v) => {
|
|
551
665
|
pattern.lastIndex = 0;
|
|
552
666
|
return pattern.test(v);
|
|
553
|
-
}, "invalid_string", message, { validation: "regex" });
|
|
667
|
+
}, "invalid_string", message, { validation: "regex" }, { pattern: pattern.source });
|
|
554
668
|
}
|
|
555
669
|
|
|
556
670
|
/**
|
|
@@ -606,6 +720,84 @@ class StringSchema extends Schema {
|
|
|
606
720
|
toUpperCase() {
|
|
607
721
|
return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toUpperCase() });
|
|
608
722
|
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A calendar date like `2024-01-31`, without a time.
|
|
726
|
+
* @param {Message} [message]
|
|
727
|
+
* @returns {this}
|
|
728
|
+
*/
|
|
729
|
+
date(message = "Invalid date, expected YYYY-MM-DD") {
|
|
730
|
+
return this._rule((v) => _DATE.test(v) && new Date(`${v}T00:00:00Z`).toISOString().startsWith(v), "invalid_string", message, { validation: "date" }, { format: "date" });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* A URL slug like `my-first-post`: lowercase letters, digits and single dashes.
|
|
735
|
+
* @param {Message} [message]
|
|
736
|
+
* @returns {this}
|
|
737
|
+
*/
|
|
738
|
+
slug(message = "Invalid slug") {
|
|
739
|
+
const { isSlug } = require("./Checker.js");
|
|
740
|
+
return this._rule(isSlug, "invalid_string", message, { validation: "slug" }, { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Hexadecimal digits only.
|
|
745
|
+
* @param {Message} [message]
|
|
746
|
+
* @returns {this}
|
|
747
|
+
*/
|
|
748
|
+
hex(message = "Invalid hexadecimal string") {
|
|
749
|
+
const { isHex } = require("./Checker.js");
|
|
750
|
+
return this._rule((v) => isHex(v), "invalid_string", message, { validation: "hex" }, { pattern: "^[0-9a-fA-F]+$" });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Base64 data.
|
|
755
|
+
* @param {Message} [message]
|
|
756
|
+
* @returns {this}
|
|
757
|
+
*/
|
|
758
|
+
base64(message = "Invalid Base64 string") {
|
|
759
|
+
const { isBase64 } = require("./Checker.js");
|
|
760
|
+
return this._rule((v) => isBase64(v), "invalid_string", message, { validation: "base64" }, { contentEncoding: "base64" });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* A ULID.
|
|
765
|
+
* @param {Message} [message]
|
|
766
|
+
* @returns {this}
|
|
767
|
+
*/
|
|
768
|
+
ulid(message = "Invalid ULID") {
|
|
769
|
+
const { isULID } = require("./Checker.js");
|
|
770
|
+
return this._rule(isULID, "invalid_string", message, { validation: "ulid" });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* A semantic version like `1.4.0` or `2.0.0-beta.1`.
|
|
775
|
+
* @param {Message} [message]
|
|
776
|
+
* @returns {this}
|
|
777
|
+
*/
|
|
778
|
+
semver(message = "Invalid version") {
|
|
779
|
+
const { isSemver } = require("./Checker.js");
|
|
780
|
+
return this._rule(isSemver, "invalid_string", message, { validation: "semver" });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* A host name like `api.example.com`.
|
|
785
|
+
* @param {Message} [message]
|
|
786
|
+
* @returns {this}
|
|
787
|
+
*/
|
|
788
|
+
hostname(message = "Invalid host name") {
|
|
789
|
+
const { isHostname } = require("./Checker.js");
|
|
790
|
+
return this._rule((v) => isHostname(v), "invalid_string", message, { validation: "hostname" }, { format: "hostname" });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* @returns {JSONSchema}
|
|
795
|
+
* @override
|
|
796
|
+
* @protected
|
|
797
|
+
*/
|
|
798
|
+
_json() {
|
|
799
|
+
return { type: "string" };
|
|
800
|
+
}
|
|
609
801
|
}
|
|
610
802
|
|
|
611
803
|
/**
|
|
@@ -642,11 +834,12 @@ class NumberSchema extends Schema {
|
|
|
642
834
|
* @param {string} code
|
|
643
835
|
* @param {Message} message
|
|
644
836
|
* @param {Record<string, unknown>} [details]
|
|
837
|
+
* @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
|
|
645
838
|
* @returns {this}
|
|
646
839
|
* @protected
|
|
647
840
|
*/
|
|
648
|
-
_rule(test, code, message, details) {
|
|
649
|
-
return this._with({ kind: "check", test, code, message, details });
|
|
841
|
+
_rule(test, code, message, details, json) {
|
|
842
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
650
843
|
}
|
|
651
844
|
|
|
652
845
|
/**
|
|
@@ -656,7 +849,7 @@ class NumberSchema extends Schema {
|
|
|
656
849
|
* @returns {this}
|
|
657
850
|
*/
|
|
658
851
|
min(value, message = `Must be greater than or equal to ${value}`) {
|
|
659
|
-
return this._rule((v) => v >= value, "too_small", message, { minimum: value });
|
|
852
|
+
return this._rule((v) => v >= value, "too_small", message, { minimum: value }, { minimum: value });
|
|
660
853
|
}
|
|
661
854
|
|
|
662
855
|
/**
|
|
@@ -666,7 +859,7 @@ class NumberSchema extends Schema {
|
|
|
666
859
|
* @returns {this}
|
|
667
860
|
*/
|
|
668
861
|
max(value, message = `Must be less than or equal to ${value}`) {
|
|
669
|
-
return this._rule((v) => v <= value, "too_big", message, { maximum: value });
|
|
862
|
+
return this._rule((v) => v <= value, "too_big", message, { maximum: value }, { maximum: value });
|
|
670
863
|
}
|
|
671
864
|
|
|
672
865
|
/**
|
|
@@ -675,7 +868,7 @@ class NumberSchema extends Schema {
|
|
|
675
868
|
* @returns {this}
|
|
676
869
|
*/
|
|
677
870
|
int(message = "Must be an integer") {
|
|
678
|
-
return this._rule(Number.isInteger, "invalid_type", message, { expected: "integer" });
|
|
871
|
+
return this._rule(Number.isInteger, "invalid_type", message, { expected: "integer" }, { type: "integer" });
|
|
679
872
|
}
|
|
680
873
|
|
|
681
874
|
/**
|
|
@@ -684,7 +877,7 @@ class NumberSchema extends Schema {
|
|
|
684
877
|
* @returns {this}
|
|
685
878
|
*/
|
|
686
879
|
positive(message = "Must be positive") {
|
|
687
|
-
return this._rule((v) => v > 0, "too_small", message, { minimum: 0, exclusive: true });
|
|
880
|
+
return this._rule((v) => v > 0, "too_small", message, { minimum: 0, exclusive: true }, { exclusiveMinimum: 0 });
|
|
688
881
|
}
|
|
689
882
|
|
|
690
883
|
/**
|
|
@@ -693,7 +886,7 @@ class NumberSchema extends Schema {
|
|
|
693
886
|
* @returns {this}
|
|
694
887
|
*/
|
|
695
888
|
nonnegative(message = "Must not be negative") {
|
|
696
|
-
return this._rule((v) => v >= 0, "too_small", message, { minimum: 0 });
|
|
889
|
+
return this._rule((v) => v >= 0, "too_small", message, { minimum: 0 }, { minimum: 0 });
|
|
697
890
|
}
|
|
698
891
|
|
|
699
892
|
/**
|
|
@@ -702,7 +895,7 @@ class NumberSchema extends Schema {
|
|
|
702
895
|
* @returns {this}
|
|
703
896
|
*/
|
|
704
897
|
negative(message = "Must be negative") {
|
|
705
|
-
return this._rule((v) => v < 0, "too_big", message, { maximum: 0, exclusive: true });
|
|
898
|
+
return this._rule((v) => v < 0, "too_big", message, { maximum: 0, exclusive: true }, { exclusiveMaximum: 0 });
|
|
706
899
|
}
|
|
707
900
|
|
|
708
901
|
/**
|
|
@@ -712,7 +905,46 @@ class NumberSchema extends Schema {
|
|
|
712
905
|
* @returns {this}
|
|
713
906
|
*/
|
|
714
907
|
multipleOf(step, message = `Must be a multiple of ${step}`) {
|
|
715
|
-
return this._rule((v) => Math.abs(v / step - Math.round(v / step)) < 1e-9, "not_multiple_of", message, { multipleOf: step });
|
|
908
|
+
return this._rule((v) => Math.abs(v / step - Math.round(v / step)) < 1e-9, "not_multiple_of", message, { multipleOf: step }, { multipleOf: step });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Greater than `value`, which itself is refused.
|
|
913
|
+
* @param {number} value
|
|
914
|
+
* @param {Message} [message]
|
|
915
|
+
* @returns {this}
|
|
916
|
+
*/
|
|
917
|
+
gt(value, message = `Must be greater than ${value}`) {
|
|
918
|
+
return this._rule((v) => v > value, "too_small", message, { minimum: value, exclusive: true }, { exclusiveMinimum: value });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Less than `value`, which itself is refused.
|
|
923
|
+
* @param {number} value
|
|
924
|
+
* @param {Message} [message]
|
|
925
|
+
* @returns {this}
|
|
926
|
+
*/
|
|
927
|
+
lt(value, message = `Must be less than ${value}`) {
|
|
928
|
+
return this._rule((v) => v < value, "too_big", message, { maximum: value, exclusive: true }, { exclusiveMaximum: value });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* An integer JavaScript can hold exactly: up to 2^53 - 1 either way.
|
|
933
|
+
* Ids from a database or an API should pass this before any arithmetic.
|
|
934
|
+
* @param {Message} [message]
|
|
935
|
+
* @returns {this}
|
|
936
|
+
*/
|
|
937
|
+
safe(message = "Must be a safe integer") {
|
|
938
|
+
return this._rule(Number.isSafeInteger, "invalid_type", message, { expected: "safe integer" }, { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* @returns {JSONSchema}
|
|
943
|
+
* @override
|
|
944
|
+
* @protected
|
|
945
|
+
*/
|
|
946
|
+
_json() {
|
|
947
|
+
return { type: "number" };
|
|
716
948
|
}
|
|
717
949
|
}
|
|
718
950
|
|
|
@@ -736,11 +968,20 @@ class BooleanSchema extends Schema {
|
|
|
736
968
|
if (typeof value === "boolean") return value;
|
|
737
969
|
if (this._coerce) {
|
|
738
970
|
const text = String(value).trim().toLowerCase();
|
|
739
|
-
if (
|
|
740
|
-
if (
|
|
971
|
+
if (_TRUE.includes(text)) return true;
|
|
972
|
+
if (_FALSE.includes(text)) return false;
|
|
741
973
|
}
|
|
742
974
|
return _typeIssue(ctx, "boolean", value, this._typeMessage);
|
|
743
975
|
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* @returns {JSONSchema}
|
|
979
|
+
* @override
|
|
980
|
+
* @protected
|
|
981
|
+
*/
|
|
982
|
+
_json() {
|
|
983
|
+
return { type: "boolean" };
|
|
984
|
+
}
|
|
744
985
|
}
|
|
745
986
|
|
|
746
987
|
/**
|
|
@@ -770,6 +1011,15 @@ class BigIntSchema extends Schema {
|
|
|
770
1011
|
}
|
|
771
1012
|
return _typeIssue(ctx, "bigint", value, this._typeMessage);
|
|
772
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* @returns {JSONSchema}
|
|
1017
|
+
* @override
|
|
1018
|
+
* @protected
|
|
1019
|
+
*/
|
|
1020
|
+
_json() {
|
|
1021
|
+
return { type: "integer" };
|
|
1022
|
+
}
|
|
773
1023
|
}
|
|
774
1024
|
|
|
775
1025
|
/**
|
|
@@ -817,6 +1067,15 @@ class DateSchema extends Schema {
|
|
|
817
1067
|
const limit = new Date(date).getTime();
|
|
818
1068
|
return this._with({ kind: "check", test: (/** @type {Date} */ v) => v.getTime() <= limit, code: "too_big", message: message ?? `Must be on or before ${new Date(limit).toISOString()}` });
|
|
819
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* @returns {JSONSchema}
|
|
1073
|
+
* @override
|
|
1074
|
+
* @protected
|
|
1075
|
+
*/
|
|
1076
|
+
_json() {
|
|
1077
|
+
return { type: "string", format: "date-time" };
|
|
1078
|
+
}
|
|
820
1079
|
}
|
|
821
1080
|
|
|
822
1081
|
/**
|
|
@@ -831,6 +1090,8 @@ class LiteralSchema extends Schema {
|
|
|
831
1090
|
super();
|
|
832
1091
|
/** @protected */
|
|
833
1092
|
this._values = values;
|
|
1093
|
+
/** @protected @type {Set<unknown>} */
|
|
1094
|
+
this._set = new Set(values);
|
|
834
1095
|
}
|
|
835
1096
|
|
|
836
1097
|
/**
|
|
@@ -841,12 +1102,23 @@ class LiteralSchema extends Schema {
|
|
|
841
1102
|
* @protected
|
|
842
1103
|
*/
|
|
843
1104
|
_check(value, ctx) {
|
|
844
|
-
|
|
1105
|
+
// A Set treats 0 and -0 as the same value: tell them apart, as before.
|
|
1106
|
+
if (this._set.has(value) && (value !== 0 || this._values.some((v) => Object.is(v, value)))) return value;
|
|
845
1107
|
const list = this._values.map((v) => JSON.stringify(v)).join(" | ");
|
|
846
1108
|
const code = this._values.length > 1 ? "invalid_enum_value" : "invalid_literal";
|
|
847
1109
|
return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${JSON.stringify(value) ?? String(value)}`, { expected: list, received: _typeName(value) });
|
|
848
1110
|
}
|
|
849
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* @returns {JSONSchema}
|
|
1114
|
+
* @override
|
|
1115
|
+
* @protected
|
|
1116
|
+
*/
|
|
1117
|
+
_json() {
|
|
1118
|
+
const values = this._values.map((v) => (typeof v === "bigint" ? Number(v) : v === undefined ? null : v));
|
|
1119
|
+
return values.length === 1 ? { const: values[0] } : values.every((v) => typeof v === "string") ? { type: "string", enum: values } : { enum: values };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
850
1122
|
/** The accepted values. */
|
|
851
1123
|
get options() {
|
|
852
1124
|
return [...this._values];
|
|
@@ -911,12 +1183,13 @@ class ArraySchema extends Schema {
|
|
|
911
1183
|
*/
|
|
912
1184
|
_check(value, ctx) {
|
|
913
1185
|
if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
|
|
914
|
-
const
|
|
1186
|
+
const item = this._item;
|
|
1187
|
+
const path = ctx.path;
|
|
1188
|
+
const out = new Array(value.length);
|
|
915
1189
|
for (let i = 0; i < value.length; i++) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
ctx.path = saved;
|
|
1190
|
+
path.push(i);
|
|
1191
|
+
out[i] = _runChild(item, value[i], ctx);
|
|
1192
|
+
path.pop();
|
|
920
1193
|
}
|
|
921
1194
|
return out;
|
|
922
1195
|
}
|
|
@@ -925,11 +1198,13 @@ class ArraySchema extends Schema {
|
|
|
925
1198
|
* @param {(v: T[]) => boolean} test
|
|
926
1199
|
* @param {string} code
|
|
927
1200
|
* @param {Message} message
|
|
1201
|
+
* @param {Record<string, unknown>} [details]
|
|
1202
|
+
* @param {Record<string, unknown>} [json]
|
|
928
1203
|
* @returns {this}
|
|
929
1204
|
* @protected
|
|
930
1205
|
*/
|
|
931
|
-
_rule(test, code, message) {
|
|
932
|
-
return this._with({ kind: "check", test, code, message });
|
|
1206
|
+
_rule(test, code, message, details, json) {
|
|
1207
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
933
1208
|
}
|
|
934
1209
|
|
|
935
1210
|
/**
|
|
@@ -939,7 +1214,7 @@ class ArraySchema extends Schema {
|
|
|
939
1214
|
* @returns {this}
|
|
940
1215
|
*/
|
|
941
1216
|
min(count, message = `Must contain at least ${count} item${count > 1 ? "s" : ""}`) {
|
|
942
|
-
return this._rule((v) => v.length >= count, "too_small", message);
|
|
1217
|
+
return this._rule((v) => v.length >= count, "too_small", message, { minimum: count }, { minItems: count });
|
|
943
1218
|
}
|
|
944
1219
|
|
|
945
1220
|
/**
|
|
@@ -949,7 +1224,7 @@ class ArraySchema extends Schema {
|
|
|
949
1224
|
* @returns {this}
|
|
950
1225
|
*/
|
|
951
1226
|
max(count, message = `Must contain at most ${count} item${count > 1 ? "s" : ""}`) {
|
|
952
|
-
return this._rule((v) => v.length <= count, "too_big", message);
|
|
1227
|
+
return this._rule((v) => v.length <= count, "too_big", message, { maximum: count }, { maxItems: count });
|
|
953
1228
|
}
|
|
954
1229
|
|
|
955
1230
|
/**
|
|
@@ -959,7 +1234,7 @@ class ArraySchema extends Schema {
|
|
|
959
1234
|
* @returns {this}
|
|
960
1235
|
*/
|
|
961
1236
|
length(count, message = `Must contain exactly ${count} items`) {
|
|
962
|
-
return this._rule((v) => v.length === count, "invalid_length", message);
|
|
1237
|
+
return this._rule((v) => v.length === count, "invalid_length", message, { exact: count }, { minItems: count, maxItems: count });
|
|
963
1238
|
}
|
|
964
1239
|
|
|
965
1240
|
/**
|
|
@@ -968,7 +1243,7 @@ class ArraySchema extends Schema {
|
|
|
968
1243
|
* @returns {this}
|
|
969
1244
|
*/
|
|
970
1245
|
nonempty(message = "Must not be empty") {
|
|
971
|
-
return this._rule((v) => v.length > 0, "too_small", message);
|
|
1246
|
+
return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minItems: 1 });
|
|
972
1247
|
}
|
|
973
1248
|
|
|
974
1249
|
/**
|
|
@@ -978,9 +1253,19 @@ class ArraySchema extends Schema {
|
|
|
978
1253
|
* @returns {this}
|
|
979
1254
|
*/
|
|
980
1255
|
unique(keyOrMessage, message = "Must not contain duplicates") {
|
|
981
|
-
const
|
|
1256
|
+
const byKey = typeof keyOrMessage === "function" && keyOrMessage.length === 1 ? /** @type {(item: T) => unknown} */ (keyOrMessage) : undefined;
|
|
982
1257
|
const msg = typeof keyOrMessage === "string" ? keyOrMessage : message;
|
|
983
|
-
return this._rule((v) => new Set(v.map(
|
|
1258
|
+
return this._rule((v) => new Set(byKey ? v.map(byKey) : v).size === v.length, "not_unique", msg, undefined, byKey ? undefined : { uniqueItems: true });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* @param {_JSONContext} ctx
|
|
1263
|
+
* @returns {JSONSchema}
|
|
1264
|
+
* @override
|
|
1265
|
+
* @protected
|
|
1266
|
+
*/
|
|
1267
|
+
_json(ctx) {
|
|
1268
|
+
return { type: "array", items: _jsonOf(this._item, ctx) };
|
|
984
1269
|
}
|
|
985
1270
|
|
|
986
1271
|
/** The schema of the items. */
|
|
@@ -1004,6 +1289,12 @@ class ObjectSchema extends Schema {
|
|
|
1004
1289
|
super();
|
|
1005
1290
|
/** @protected */
|
|
1006
1291
|
this._shape = shape;
|
|
1292
|
+
// Read once here rather than on every parse. A key named __proto__ could
|
|
1293
|
+
// only ever replace the prototype of the result, so it's left out.
|
|
1294
|
+
/** @protected @type {string[]} */
|
|
1295
|
+
this._keys = Object.keys(shape).filter((key) => key !== "__proto__");
|
|
1296
|
+
/** @protected @type {Array<Schema<any>>} */
|
|
1297
|
+
this._schemas = this._keys.map((key) => shape[key]);
|
|
1007
1298
|
}
|
|
1008
1299
|
|
|
1009
1300
|
/**
|
|
@@ -1016,22 +1307,65 @@ class ObjectSchema extends Schema {
|
|
|
1016
1307
|
_check(value, ctx) {
|
|
1017
1308
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1018
1309
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1310
|
+
const keys = this._keys;
|
|
1311
|
+
const schemas = this._schemas;
|
|
1312
|
+
const path = ctx.path;
|
|
1019
1313
|
/** @type {Record<string, unknown>} */
|
|
1020
1314
|
const out = {};
|
|
1021
|
-
for (
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1315
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1316
|
+
const key = keys[i];
|
|
1317
|
+
let raw = input[key];
|
|
1318
|
+
// What every object inherits, like toString, is not data.
|
|
1319
|
+
if (typeof raw === "function" && !Object.hasOwn(input, key)) raw = undefined;
|
|
1320
|
+
path.push(key);
|
|
1321
|
+
const result = _runChild(schemas[i], raw, ctx);
|
|
1322
|
+
path.pop();
|
|
1323
|
+
if (result !== undefined || Object.hasOwn(input, key)) out[key] = result;
|
|
1027
1324
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1325
|
+
if (this._unknown === "strip") return out;
|
|
1326
|
+
/** @type {string[] | undefined} */
|
|
1327
|
+
let unknown;
|
|
1328
|
+
for (const key of Object.keys(input)) {
|
|
1329
|
+
if (Object.hasOwn(this._shape, key)) continue;
|
|
1330
|
+
if (this._unknown === "strict") (unknown ??= []).push(key);
|
|
1331
|
+
else if (!UNSAFE_KEYS.has(key)) out[key] = input[key];
|
|
1034
1332
|
}
|
|
1333
|
+
if (unknown) _issue(ctx, "unrecognized_keys", `Unrecognized key${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}`, { keys: unknown });
|
|
1334
|
+
return out;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* A schema with another shape that keeps how this one treats unknown keys.
|
|
1339
|
+
* @param {Shape} shape
|
|
1340
|
+
* @returns {any}
|
|
1341
|
+
* @protected
|
|
1342
|
+
*/
|
|
1343
|
+
_reshape(shape) {
|
|
1344
|
+
const copy = new ObjectSchema(shape);
|
|
1345
|
+
copy._unknown = this._unknown;
|
|
1346
|
+
return copy;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* @param {_JSONContext} ctx
|
|
1351
|
+
* @returns {JSONSchema}
|
|
1352
|
+
* @override
|
|
1353
|
+
* @protected
|
|
1354
|
+
*/
|
|
1355
|
+
_json(ctx) {
|
|
1356
|
+
/** @type {Record<string, JSONSchema>} */
|
|
1357
|
+
const properties = {};
|
|
1358
|
+
const required = [];
|
|
1359
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
1360
|
+
const child = /** @type {any} */ (this._schemas[i]);
|
|
1361
|
+
properties[this._keys[i]] = _jsonOf(child, ctx);
|
|
1362
|
+
// any() and unknown() take undefined too, so a key of theirs can be left out
|
|
1363
|
+
if (!child._optional && !child._default && child.constructor !== Schema) required.push(this._keys[i]);
|
|
1364
|
+
}
|
|
1365
|
+
/** @type {JSONSchema} */
|
|
1366
|
+
const out = { type: "object", properties };
|
|
1367
|
+
if (required.length) out.required = required;
|
|
1368
|
+
if (this._unknown === "strict") out.additionalProperties = false;
|
|
1035
1369
|
return out;
|
|
1036
1370
|
}
|
|
1037
1371
|
|
|
@@ -1067,9 +1401,7 @@ class ObjectSchema extends Schema {
|
|
|
1067
1401
|
* @returns {ObjectSchema<Omit<S, keyof M> & M>}
|
|
1068
1402
|
*/
|
|
1069
1403
|
extend(more) {
|
|
1070
|
-
|
|
1071
|
-
copy._unknown = this._unknown;
|
|
1072
|
-
return copy;
|
|
1404
|
+
return this._reshape({ ...this._shape, ...more });
|
|
1073
1405
|
}
|
|
1074
1406
|
|
|
1075
1407
|
/**
|
|
@@ -1082,7 +1414,7 @@ class ObjectSchema extends Schema {
|
|
|
1082
1414
|
/** @type {any} */
|
|
1083
1415
|
const shape = {};
|
|
1084
1416
|
for (const key of keys) shape[key] = this._shape[key];
|
|
1085
|
-
return
|
|
1417
|
+
return this._reshape(shape);
|
|
1086
1418
|
}
|
|
1087
1419
|
|
|
1088
1420
|
/**
|
|
@@ -1095,7 +1427,7 @@ class ObjectSchema extends Schema {
|
|
|
1095
1427
|
/** @type {any} */
|
|
1096
1428
|
const shape = { ...this._shape };
|
|
1097
1429
|
for (const key of keys) delete shape[key];
|
|
1098
|
-
return
|
|
1430
|
+
return this._reshape(shape);
|
|
1099
1431
|
}
|
|
1100
1432
|
|
|
1101
1433
|
/**
|
|
@@ -1105,8 +1437,54 @@ class ObjectSchema extends Schema {
|
|
|
1105
1437
|
partial() {
|
|
1106
1438
|
/** @type {any} */
|
|
1107
1439
|
const shape = {};
|
|
1108
|
-
for (
|
|
1109
|
-
return
|
|
1440
|
+
for (let i = 0; i < this._keys.length; i++) shape[this._keys[i]] = this._schemas[i].optional();
|
|
1441
|
+
return this._reshape(shape);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Makes every key required again: the opposite of `partial()`.
|
|
1446
|
+
* @returns {ObjectSchema<{ [K in keyof S]: Schema<Exclude<Infer<S[K]>, undefined>> }>}
|
|
1447
|
+
*/
|
|
1448
|
+
required() {
|
|
1449
|
+
/** @type {any} */
|
|
1450
|
+
const shape = {};
|
|
1451
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
1452
|
+
const copy = /** @type {any} */ (this._schemas[i])._clone();
|
|
1453
|
+
copy._optional = false;
|
|
1454
|
+
shape[this._keys[i]] = copy;
|
|
1455
|
+
}
|
|
1456
|
+
return this._reshape(shape);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Combines two object schemas. Keys of the other one win, and so does
|
|
1461
|
+
* its way of treating unknown keys.
|
|
1462
|
+
*
|
|
1463
|
+
* @example
|
|
1464
|
+
* const Timestamps = s.object({ createdAt: s.date(), updatedAt: s.date() });
|
|
1465
|
+
* const Post = s.object({ title: s.string() }).merge(Timestamps);
|
|
1466
|
+
*
|
|
1467
|
+
* @template {Shape} M
|
|
1468
|
+
* @param {ObjectSchema<M>} other
|
|
1469
|
+
* @returns {ObjectSchema<Omit<S, keyof M> & M>}
|
|
1470
|
+
*/
|
|
1471
|
+
merge(other) {
|
|
1472
|
+
const copy = /** @type {any} */ (new ObjectSchema({ ...this._shape, ...other._shape }));
|
|
1473
|
+
copy._unknown = other._unknown;
|
|
1474
|
+
return copy;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* A schema that accepts the name of any key: handy for a `sort` or
|
|
1479
|
+
* `fields` query parameter.
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* const SortBy = User.keyof(); // "name" | "email" | "age" | "role"
|
|
1483
|
+
*
|
|
1484
|
+
* @returns {LiteralSchema<keyof S & string>}
|
|
1485
|
+
*/
|
|
1486
|
+
keyof() {
|
|
1487
|
+
return new LiteralSchema(/** @type {Array<keyof S & string>} */ (this._keys));
|
|
1110
1488
|
}
|
|
1111
1489
|
}
|
|
1112
1490
|
|
|
@@ -1149,6 +1527,75 @@ class UnionSchema extends Schema {
|
|
|
1149
1527
|
ctx.issues.push(...best);
|
|
1150
1528
|
return INVALID;
|
|
1151
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* @param {_JSONContext} ctx
|
|
1533
|
+
* @returns {JSONSchema}
|
|
1534
|
+
* @override
|
|
1535
|
+
* @protected
|
|
1536
|
+
*/
|
|
1537
|
+
_json(ctx) {
|
|
1538
|
+
return { anyOf: this._options.map((option) => _jsonOf(option, ctx)) };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* A union of objects told apart by one key.
|
|
1544
|
+
*
|
|
1545
|
+
* @template T
|
|
1546
|
+
* @extends {Schema<T>}
|
|
1547
|
+
*/
|
|
1548
|
+
class DiscriminatedUnionSchema extends Schema {
|
|
1549
|
+
/**
|
|
1550
|
+
* @param {string} key
|
|
1551
|
+
* @param {ReadonlyArray<ObjectSchema<any>>} options
|
|
1552
|
+
*/
|
|
1553
|
+
constructor(key, options) {
|
|
1554
|
+
super();
|
|
1555
|
+
/** @protected */
|
|
1556
|
+
this._key = key;
|
|
1557
|
+
/** @protected */
|
|
1558
|
+
this._options = options;
|
|
1559
|
+
/** @protected @type {Map<unknown, Schema<any>>} */
|
|
1560
|
+
this._byValue = new Map();
|
|
1561
|
+
for (const option of options) {
|
|
1562
|
+
const values = /** @type {any} */ (option)?._shape?.[key]?._values;
|
|
1563
|
+
if (!Array.isArray(values)) throw new TypeError(`discriminatedUnion(): every option must be an object with a literal "${key}"`);
|
|
1564
|
+
for (const value of values) {
|
|
1565
|
+
if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${JSON.stringify(value)}`);
|
|
1566
|
+
this._byValue.set(value, option);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* @param {unknown} value
|
|
1573
|
+
* @param {ParseContext} ctx
|
|
1574
|
+
* @returns {unknown}
|
|
1575
|
+
* @override
|
|
1576
|
+
* @protected
|
|
1577
|
+
*/
|
|
1578
|
+
_check(value, ctx) {
|
|
1579
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1580
|
+
const tag = /** @type {Record<string, unknown>} */ (value)[this._key];
|
|
1581
|
+
const option = this._byValue.get(tag);
|
|
1582
|
+
if (option) return _runChild(option, value, ctx);
|
|
1583
|
+
const expected = [...this._byValue.keys()].map((known) => JSON.stringify(known)).join(" | ");
|
|
1584
|
+
ctx.path.push(this._key);
|
|
1585
|
+
_issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${JSON.stringify(tag) ?? String(tag)}`, { expected, received: _typeName(tag) });
|
|
1586
|
+
ctx.path.pop();
|
|
1587
|
+
return INVALID;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* @param {_JSONContext} ctx
|
|
1592
|
+
* @returns {JSONSchema}
|
|
1593
|
+
* @override
|
|
1594
|
+
* @protected
|
|
1595
|
+
*/
|
|
1596
|
+
_json(ctx) {
|
|
1597
|
+
return { oneOf: this._options.map((option) => _jsonOf(option, ctx)), discriminator: { propertyName: this._key } };
|
|
1598
|
+
}
|
|
1152
1599
|
}
|
|
1153
1600
|
|
|
1154
1601
|
/**
|
|
@@ -1177,13 +1624,24 @@ class TupleSchema extends Schema {
|
|
|
1177
1624
|
if (value.length !== this._items.length) {
|
|
1178
1625
|
return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
|
|
1179
1626
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1627
|
+
const path = ctx.path;
|
|
1628
|
+
const out = new Array(this._items.length);
|
|
1629
|
+
for (let i = 0; i < out.length; i++) {
|
|
1630
|
+
path.push(i);
|
|
1631
|
+
out[i] = _runChild(this._items[i], value[i], ctx);
|
|
1632
|
+
path.pop();
|
|
1633
|
+
}
|
|
1634
|
+
return out;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* @param {_JSONContext} ctx
|
|
1639
|
+
* @returns {JSONSchema}
|
|
1640
|
+
* @override
|
|
1641
|
+
* @protected
|
|
1642
|
+
*/
|
|
1643
|
+
_json(ctx) {
|
|
1644
|
+
return { type: "array", prefixItems: this._items.map((item) => _jsonOf(item, ctx)), items: false, minItems: this._items.length, maxItems: this._items.length };
|
|
1187
1645
|
}
|
|
1188
1646
|
}
|
|
1189
1647
|
|
|
@@ -1215,19 +1673,33 @@ class RecordSchema extends Schema {
|
|
|
1215
1673
|
*/
|
|
1216
1674
|
_check(value, ctx) {
|
|
1217
1675
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1676
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1677
|
+
const path = ctx.path;
|
|
1218
1678
|
/** @type {Record<string, unknown>} */
|
|
1219
1679
|
const out = {};
|
|
1220
|
-
for (const
|
|
1221
|
-
if (key
|
|
1222
|
-
|
|
1223
|
-
ctx.path = [...saved, key];
|
|
1680
|
+
for (const key of Object.keys(input)) {
|
|
1681
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
1682
|
+
path.push(key);
|
|
1224
1683
|
const k = this._keys ? _runChild(this._keys, key, ctx) : key;
|
|
1225
|
-
const v = _runChild(this._values,
|
|
1226
|
-
|
|
1227
|
-
if (typeof k === "string") out[k] = v;
|
|
1684
|
+
const v = _runChild(this._values, input[key], ctx);
|
|
1685
|
+
path.pop();
|
|
1686
|
+
if (typeof k === "string" && !UNSAFE_KEYS.has(k)) out[k] = v;
|
|
1228
1687
|
}
|
|
1229
1688
|
return out;
|
|
1230
1689
|
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* @param {_JSONContext} ctx
|
|
1693
|
+
* @returns {JSONSchema}
|
|
1694
|
+
* @override
|
|
1695
|
+
* @protected
|
|
1696
|
+
*/
|
|
1697
|
+
_json(ctx) {
|
|
1698
|
+
/** @type {JSONSchema} */
|
|
1699
|
+
const out = { type: "object", additionalProperties: _jsonOf(this._values, ctx) };
|
|
1700
|
+
if (this._keys) out.propertyNames = _jsonOf(this._keys, ctx);
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1231
1703
|
}
|
|
1232
1704
|
|
|
1233
1705
|
/**
|
|
@@ -1242,6 +1714,8 @@ class LazySchema extends Schema {
|
|
|
1242
1714
|
super();
|
|
1243
1715
|
/** @protected */
|
|
1244
1716
|
this._getter = getter;
|
|
1717
|
+
/** @protected @type {Schema<T> | undefined} */
|
|
1718
|
+
this._resolved = undefined;
|
|
1245
1719
|
}
|
|
1246
1720
|
|
|
1247
1721
|
/**
|
|
@@ -1253,9 +1727,28 @@ class LazySchema extends Schema {
|
|
|
1253
1727
|
*/
|
|
1254
1728
|
_check(value, ctx) {
|
|
1255
1729
|
const before = ctx.issues.length;
|
|
1256
|
-
const out = _runChild(this._getter(), value, ctx);
|
|
1730
|
+
const out = _runChild((this._resolved ??= this._getter()), value, ctx);
|
|
1257
1731
|
return ctx.issues.length > before ? INVALID : out;
|
|
1258
1732
|
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* A schema that refers to itself can't be written inline: it goes under
|
|
1736
|
+
* `$defs` and is pointed at.
|
|
1737
|
+
* @param {_JSONContext} ctx
|
|
1738
|
+
* @returns {JSONSchema}
|
|
1739
|
+
* @override
|
|
1740
|
+
* @protected
|
|
1741
|
+
*/
|
|
1742
|
+
_json(ctx) {
|
|
1743
|
+
const target = (this._resolved ??= this._getter());
|
|
1744
|
+
let name = ctx.names.get(target);
|
|
1745
|
+
if (!name) {
|
|
1746
|
+
name = `schema${ctx.names.size + 1}`;
|
|
1747
|
+
ctx.names.set(target, name);
|
|
1748
|
+
ctx.defs[name] = _jsonOf(target, ctx);
|
|
1749
|
+
}
|
|
1750
|
+
return { $ref: `#/$defs/${name}` };
|
|
1751
|
+
}
|
|
1259
1752
|
}
|
|
1260
1753
|
|
|
1261
1754
|
/**
|
|
@@ -1403,6 +1896,27 @@ function union(options) {
|
|
|
1403
1896
|
return new UnionSchema(options);
|
|
1404
1897
|
}
|
|
1405
1898
|
|
|
1899
|
+
/**
|
|
1900
|
+
* A union of objects told apart by one key, like the `type` of an event.
|
|
1901
|
+
* Unlike `union()`, it goes straight to the right schema, and a wrong
|
|
1902
|
+
* object gets the errors of the kind it claims to be.
|
|
1903
|
+
*
|
|
1904
|
+
* @example
|
|
1905
|
+
* const Event = s.discriminatedUnion("type", [
|
|
1906
|
+
* s.object({ type: s.literal("click"), x: s.number(), y: s.number() }),
|
|
1907
|
+
* s.object({ type: s.literal("key"), key: s.string() }),
|
|
1908
|
+
* ]);
|
|
1909
|
+
* Event.parse({ type: "key", key: "Enter" });
|
|
1910
|
+
*
|
|
1911
|
+
* @template {ReadonlyArray<ObjectSchema<any>>} U
|
|
1912
|
+
* @param {string} key The key every option has, with a different literal in each.
|
|
1913
|
+
* @param {U} options
|
|
1914
|
+
* @returns {DiscriminatedUnionSchema<Infer<U[number]>>}
|
|
1915
|
+
*/
|
|
1916
|
+
function discriminatedUnion(key, options) {
|
|
1917
|
+
return new DiscriminatedUnionSchema(key, options);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1406
1920
|
/**
|
|
1407
1921
|
* A fixed-length array with one schema per position.
|
|
1408
1922
|
*
|
|
@@ -1574,6 +2088,7 @@ module.exports = {
|
|
|
1574
2088
|
array,
|
|
1575
2089
|
object,
|
|
1576
2090
|
union,
|
|
2091
|
+
discriminatedUnion,
|
|
1577
2092
|
tuple,
|
|
1578
2093
|
record,
|
|
1579
2094
|
lazy,
|