@ix-xs/node-comfort 1.2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Schema.js ADDED
@@ -0,0 +1,2101 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Validate data with schemas, in the spirit of `zod`. Describe your data
5
+ * once and get both a runtime check with clear messages and the TypeScript
6
+ * type, `Infer<typeof schema>`. Schemas never change: `.min()` or
7
+ * `.optional()` return a new one.
8
+ *
9
+ * @example
10
+ * const User = s.object({
11
+ * name: s.string().trim().min(2),
12
+ * email: s.string().email().toLowerCase(),
13
+ * age: s.number().int().min(18).optional(),
14
+ * role: s.enum(["admin", "user"]).default("user"),
15
+ * });
16
+ *
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, issues }
19
+ */
20
+
21
+ const { types } = require("node:util");
22
+ const { ValidationError } = require("./errors.js");
23
+ const { UNSAFE_KEYS } = require("./internal/shared.js");
24
+
25
+ /**
26
+ * The type a schema produces.
27
+ *
28
+ * @example
29
+ * // type User = Infer<typeof UserSchema>;
30
+ *
31
+ * @template S
32
+ * @typedef {S extends Schema<infer T> ? T : never} Infer
33
+ */
34
+
35
+ /**
36
+ * An object of schemas, as passed to `object()`.
37
+ * @typedef {Record<string, Schema<any>>} Shape
38
+ */
39
+
40
+ /**
41
+ * Flattens a type so editors show it nicely.
42
+ * @template T
43
+ * @typedef {{ [K in keyof T]: T[K] } & {}} Simplify
44
+ */
45
+
46
+ /**
47
+ * The type of an object schema. Keys that accept `undefined` become optional.
48
+ * @template {Shape} S
49
+ * @typedef {Simplify<{ [K in keyof S as undefined extends Infer<S[K]> ? never : K]: Infer<S[K]> } & { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]> }>} ObjectOutput
50
+ */
51
+
52
+ /**
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.
55
+ * @template T
56
+ * @typedef {{ success: true, data: T, error?: undefined, issues?: undefined } | { success: false, data?: undefined, error: ValidationError, issues: import("./errors").ValidationIssue[] }} SafeParseResult
57
+ */
58
+
59
+ /**
60
+ * An error message: a string, or a function that gets the invalid value.
61
+ * @typedef {string | ((value: any) => string)} Message
62
+ */
63
+
64
+ /**
65
+ * Options for `refine()`.
66
+ * @typedef {object} RefineOptions
67
+ * @property {Message} [message] Defaults to `"Invalid value"`.
68
+ * @property {Array<string | number>} [path] Where to report the issue, like `["confirmPassword"]` for a cross-field check.
69
+ * @property {string} [code] Defaults to `"custom"`.
70
+ */
71
+
72
+ /**
73
+ * @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number> }} ParseContext
74
+ */
75
+
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
89
+ */
90
+
91
+ const INVALID = Symbol("invalid");
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
+
116
+ /** @param {unknown} value */
117
+ const _typeName = (value) => {
118
+ if (value === null) return "null";
119
+ if (Array.isArray(value)) return "array";
120
+ if (types.isDate(value)) return Number.isNaN(value.getTime()) ? "invalid date" : "date";
121
+ if (typeof value === "number" && Number.isNaN(value)) return "NaN";
122
+ return typeof value;
123
+ };
124
+
125
+ /**
126
+ * @param {Message} message
127
+ * @param {unknown} value
128
+ */
129
+ const _message = (message, value) => (typeof message === "function" ? message(value) : message);
130
+
131
+ /**
132
+ * @param {ParseContext} ctx
133
+ * @param {string} code
134
+ * @param {string} message
135
+ * @param {Record<string, unknown>} [details]
136
+ */
137
+ const _issue = (ctx, code, message, details) => {
138
+ ctx.issues.push({ path: [...ctx.path], code, message, ...details });
139
+ };
140
+
141
+ /**
142
+ * @param {ParseContext} ctx
143
+ * @param {string} expected
144
+ * @param {unknown} value
145
+ * @param {Message} [message]
146
+ */
147
+ const _typeIssue = (ctx, expected, value, message) => {
148
+ const received = _typeName(value);
149
+ /** @protected */
150
+ _issue(ctx, "invalid_type", message ? _message(message, value) : value === undefined ? "Required" : `Expected ${expected}, received ${received}`, { expected, received });
151
+ };
152
+
153
+ /**
154
+ * Runs a nested schema.
155
+ * @param {Schema<any>} schema
156
+ * @param {unknown} value
157
+ * @param {ParseContext} ctx
158
+ * @returns {unknown}
159
+ */
160
+ const _runChild = (schema, value, ctx) => /** @type {any} */ (schema)._run(value, ctx);
161
+
162
+ /**
163
+ * The base of every schema.
164
+ *
165
+ * @template [T=unknown]
166
+ */
167
+ class Schema {
168
+ /** @protected @type {Step[]} */
169
+ _steps = [];
170
+ /** @protected */
171
+ _optional = false;
172
+ /** @protected */
173
+ _nullable = false;
174
+ /** @protected @type {{ value: unknown } | undefined} */
175
+ _default = undefined;
176
+ /** @protected @type {{ value: unknown } | undefined} */
177
+ _catch = undefined;
178
+ /** @protected @type {Message | undefined} */
179
+ _typeMessage = undefined;
180
+ /** @protected @type {string | undefined} */
181
+ _description = undefined;
182
+
183
+ /**
184
+ * Checks the type, and coerces when asked. Subclasses override it.
185
+ * @param {unknown} value
186
+ * @param {ParseContext} _ctx
187
+ * @returns {unknown}
188
+ * @protected
189
+ */
190
+ _check(value, _ctx) {
191
+ return value;
192
+ }
193
+
194
+ /**
195
+ * @returns {this}
196
+ * @protected
197
+ */
198
+ _clone() {
199
+ const copy = Object.create(Object.getPrototypeOf(this));
200
+ Object.assign(copy, this);
201
+ copy._steps = [...this._steps];
202
+ return copy;
203
+ }
204
+
205
+ /**
206
+ * @param {Step} step
207
+ * @returns {this}
208
+ * @protected
209
+ */
210
+ _with(step) {
211
+ const copy = this._clone();
212
+ copy._steps.push(step);
213
+ return copy;
214
+ }
215
+
216
+ /**
217
+ * @param {unknown} input
218
+ * @param {ParseContext} ctx
219
+ * @returns {unknown} The output, or `INVALID`.
220
+ * @protected
221
+ */
222
+ _run(input, ctx) {
223
+ let value = input;
224
+ if (value === undefined && this._default) {
225
+ const def = this._default.value;
226
+ value = typeof def === "function" ? def() : def !== null && typeof def === "object" ? require("./Obj.js").clone(def) : def;
227
+ }
228
+ if (value === undefined && this._optional) return undefined;
229
+ if (value === null && this._nullable) return null;
230
+ const before = ctx.issues.length;
231
+ let out = this._check(value, ctx);
232
+ if (ctx.issues.length === before) {
233
+ for (const step of this._steps) {
234
+ if (step.kind === "check") {
235
+ if (!step.test(out)) _issue(ctx, step.code, _message(step.message, out), step.details);
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)) {
244
+ const saved = ctx.path;
245
+ if (step.options.path) ctx.path = [...saved, ...step.options.path];
246
+ _issue(ctx, step.options.code ?? "custom", _message(step.options.message ?? "Invalid value", out));
247
+ ctx.path = saved;
248
+ }
249
+ }
250
+ }
251
+ if (ctx.issues.length > before) {
252
+ if (this._catch) {
253
+ ctx.issues.length = before;
254
+ const fallback = this._catch.value;
255
+ return typeof fallback === "function" ? fallback(input) : fallback;
256
+ }
257
+ return INVALID;
258
+ }
259
+ return out;
260
+ }
261
+
262
+ /**
263
+ * Validates a value and returns it, with defaults and transformations
264
+ * applied. Unknown object keys are dropped.
265
+ *
266
+ * @example
267
+ * const port = s.coerce.number().int().min(1).max(65535).parse(process.env.PORT);
268
+ *
269
+ * @param {unknown} value
270
+ * @returns {T}
271
+ * @throws {ValidationError} Listing every issue with its path.
272
+ */
273
+ parse(value) {
274
+ const ctx = { issues: [], path: [] };
275
+ const out = this._run(value, ctx);
276
+ if (ctx.issues.length) throw new ValidationError(ctx.issues);
277
+ return /** @type {T} */ (out);
278
+ }
279
+
280
+ /**
281
+ * Validates a value without throwing.
282
+ *
283
+ * @example
284
+ * const result = User.safeParse(req.body);
285
+ * if (!result.success) return res.status(400).json(result.error.flatten());
286
+ * saveUser(result.data);
287
+ *
288
+ * @param {unknown} value
289
+ * @returns {SafeParseResult<T>}
290
+ */
291
+ safeParse(value) {
292
+ /** @type {ParseContext} */
293
+ const ctx = { issues: [], path: [] };
294
+ const out = this._run(value, ctx);
295
+ if (ctx.issues.length) return _failure(ctx.issues);
296
+ return { success: true, data: /** @type {T} */ (out) };
297
+ }
298
+
299
+ /**
300
+ * Is the value valid? Works as a type guard, and builds no error.
301
+ *
302
+ * @example
303
+ * if (Email.is(input)) sendMail(input); // input is a string here
304
+ *
305
+ * @param {unknown} value
306
+ * @returns {value is T}
307
+ */
308
+ is(value) {
309
+ /** @type {ParseContext} */
310
+ const ctx = { issues: [], path: [] };
311
+ this._run(value, ctx);
312
+ return ctx.issues.length === 0;
313
+ }
314
+
315
+ /**
316
+ * Also accepts `undefined`. In an object, the key becomes optional.
317
+ *
318
+ * @example
319
+ * s.object({ nickname: s.string().optional() }); // { nickname?: string }
320
+ *
321
+ * @returns {Schema<T | undefined>}
322
+ */
323
+ optional() {
324
+ const copy = this._clone();
325
+ copy._optional = true;
326
+ return copy;
327
+ }
328
+
329
+ /**
330
+ * Also accepts `null`.
331
+ *
332
+ * @returns {Schema<T | null>}
333
+ */
334
+ nullable() {
335
+ const copy = this._clone();
336
+ copy._nullable = true;
337
+ return copy;
338
+ }
339
+
340
+ /**
341
+ * Also accepts `null` and `undefined`.
342
+ *
343
+ * @returns {Schema<T | null | undefined>}
344
+ */
345
+ nullish() {
346
+ const copy = this._clone();
347
+ copy._nullable = true;
348
+ copy._optional = true;
349
+ return copy;
350
+ }
351
+
352
+ /**
353
+ * Uses this value when the input is `undefined`. Objects are copied each
354
+ * time; pass a function to build a fresh value on every parse.
355
+ *
356
+ * @example
357
+ * s.string().default("guest");
358
+ * s.date().default(() => new Date());
359
+ *
360
+ * @param {Exclude<T, undefined> | (() => Exclude<T, undefined>)} value
361
+ * @returns {Schema<Exclude<T, undefined>>}
362
+ */
363
+ default(value) {
364
+ const copy = this._clone();
365
+ copy._default = { value };
366
+ return /** @type {any} */ (copy);
367
+ }
368
+
369
+ /**
370
+ * Returns this value instead of failing when the input is invalid.
371
+ *
372
+ * @example
373
+ * s.number().catch(0).parse("not a number"); // 0
374
+ *
375
+ * @param {T | ((input: unknown) => T)} value A value, or a function that gets the invalid input.
376
+ * @returns {Schema<T>}
377
+ */
378
+ catch(value) {
379
+ const copy = this._clone();
380
+ copy._catch = { value };
381
+ return /** @type {any} */ (copy);
382
+ }
383
+
384
+ /**
385
+ * Changes the value once it's valid. Runs after the checks written before it.
386
+ *
387
+ * @example
388
+ * s.string().transform((value) => value.split(",")); // Schema<string[]>
389
+ *
390
+ * @template U
391
+ * @param {(value: T) => U} fn
392
+ * @returns {Schema<U>}
393
+ */
394
+ transform(fn) {
395
+ return /** @type {any} */ (this._with({ kind: "transform", fn }));
396
+ }
397
+
398
+ /**
399
+ * Adds your own rule.
400
+ *
401
+ * @example
402
+ * s.object({ password: s.string(), confirm: s.string() })
403
+ * .refine((v) => v.password === v.confirm, { message: "Passwords don't match", path: ["confirm"] });
404
+ *
405
+ * @param {(value: T) => boolean} test Returns `true` when the value is fine.
406
+ * @param {Message | RefineOptions} [options]
407
+ * @returns {this}
408
+ */
409
+ refine(test, options = {}) {
410
+ const opts = typeof options === "string" || typeof options === "function" ? { message: options } : options;
411
+ return this._with({ kind: "refine", test, options: opts });
412
+ }
413
+
414
+ /**
415
+ * Accepts values that match this schema or another one.
416
+ *
417
+ * @example
418
+ * s.string().or(s.number()); // Schema<string | number>
419
+ *
420
+ * @template U
421
+ * @param {Schema<U>} other
422
+ * @returns {Schema<T | U>}
423
+ */
424
+ or(other) {
425
+ return union([/** @type {Schema<T>} */ (this), other]);
426
+ }
427
+
428
+ /**
429
+ * An array of this schema.
430
+ *
431
+ * @example
432
+ * s.string().email().array(); // Schema<string[]>
433
+ *
434
+ * @returns {ArraySchema<T>}
435
+ */
436
+ array() {
437
+ return array(/** @type {Schema<T>} */ (this));
438
+ }
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
+
510
+ /**
511
+ * Attaches a description, available as `.description`. Useful to generate
512
+ * docs or forms.
513
+ *
514
+ * @param {string} text
515
+ * @returns {this}
516
+ */
517
+ describe(text) {
518
+ const copy = this._clone();
519
+ copy._description = text;
520
+ return copy;
521
+ }
522
+
523
+ /** The text set with `describe()`. */
524
+ get description() {
525
+ return this._description;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * A string schema.
531
+ *
532
+ * @extends {Schema<string>}
533
+ */
534
+ class StringSchema extends Schema {
535
+ /** @protected */
536
+ _coerce = false;
537
+
538
+ /**
539
+ * @param {unknown} value
540
+ * @param {ParseContext} ctx
541
+ * @returns {unknown}
542
+ * @override
543
+ * @protected
544
+ */
545
+ _check(value, ctx) {
546
+ let v = value;
547
+ if (this._coerce && v !== undefined && v !== null && typeof v !== "string") v = String(v);
548
+ if (typeof v !== "string") return _typeIssue(ctx, "string", value, this._typeMessage);
549
+ return v;
550
+ }
551
+
552
+ /**
553
+ * @param {(v: string) => boolean} test
554
+ * @param {string} code
555
+ * @param {Message} message
556
+ * @param {Record<string, unknown>} [details]
557
+ * @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
558
+ * @returns {this}
559
+ * @protected
560
+ */
561
+ _rule(test, code, message, details, json) {
562
+ return this._with({ kind: "check", test, code, message, details, json });
563
+ }
564
+
565
+ /**
566
+ * At least `length` characters.
567
+ * @param {number} length
568
+ * @param {Message} [message]
569
+ * @returns {this}
570
+ */
571
+ min(length, message = `Must contain at least ${length} character${length > 1 ? "s" : ""}`) {
572
+ return this._rule((v) => v.length >= length, "too_small", message, { minimum: length }, { minLength: length });
573
+ }
574
+
575
+ /**
576
+ * At most `length` characters.
577
+ * @param {number} length
578
+ * @param {Message} [message]
579
+ * @returns {this}
580
+ */
581
+ max(length, message = `Must contain at most ${length} character${length > 1 ? "s" : ""}`) {
582
+ return this._rule((v) => v.length <= length, "too_big", message, { maximum: length }, { maxLength: length });
583
+ }
584
+
585
+ /**
586
+ * Exactly `length` characters.
587
+ * @param {number} length
588
+ * @param {Message} [message]
589
+ * @returns {this}
590
+ */
591
+ length(length, message = `Must contain exactly ${length} characters`) {
592
+ return this._rule((v) => v.length === length, "invalid_length", message, { exact: length }, { minLength: length, maxLength: length });
593
+ }
594
+
595
+ /**
596
+ * Not the empty string.
597
+ * @param {Message} [message]
598
+ * @returns {this}
599
+ */
600
+ nonempty(message = "Must not be empty") {
601
+ return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minLength: 1 });
602
+ }
603
+
604
+ /**
605
+ * A valid email address.
606
+ * @param {Message} [message]
607
+ * @returns {this}
608
+ */
609
+ email(message = "Invalid email address") {
610
+ const { isEmail } = require("./Checker.js");
611
+ return this._rule(isEmail, "invalid_string", message, { validation: "email" }, { format: "email" });
612
+ }
613
+
614
+ /**
615
+ * An absolute URL.
616
+ * @param {Message | { message?: Message, protocols?: string[] }} [options] A message, or allowed protocols (http and https by default).
617
+ * @returns {this}
618
+ */
619
+ url(options = "Invalid URL") {
620
+ const opts = typeof options === "object" ? options : { message: options };
621
+ 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" });
623
+ }
624
+
625
+ /**
626
+ * A UUID.
627
+ * @param {Message} [message]
628
+ * @returns {this}
629
+ */
630
+ uuid(message = "Invalid UUID") {
631
+ const { isUUID } = require("./Checker.js");
632
+ return this._rule((v) => isUUID(v), "invalid_string", message, { validation: "uuid" }, { format: "uuid" });
633
+ }
634
+
635
+ /**
636
+ * An IP address.
637
+ * @param {4 | 6 | Message} [versionOrMessage] Only IPv4 or IPv6, or a message.
638
+ * @param {Message} [message]
639
+ * @returns {this}
640
+ */
641
+ ip(versionOrMessage, message = "Invalid IP address") {
642
+ const version = typeof versionOrMessage === "number" ? versionOrMessage : undefined;
643
+ const { isIP } = require("./Checker.js");
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);
645
+ }
646
+
647
+ /**
648
+ * An ISO 8601 date or date-time, like `2024-01-31T10:00:00Z`.
649
+ * @param {Message} [message]
650
+ * @returns {this}
651
+ */
652
+ datetime(message = "Invalid ISO date") {
653
+ const { isISODate } = require("./Checker.js");
654
+ return this._rule(isISODate, "invalid_string", message, { validation: "datetime" }, { format: "date-time" });
655
+ }
656
+
657
+ /**
658
+ * Matches a regular expression.
659
+ * @param {RegExp} pattern
660
+ * @param {Message} [message]
661
+ * @returns {this}
662
+ */
663
+ regex(pattern, message = `Must match ${pattern}`) {
664
+ return this._rule((v) => {
665
+ pattern.lastIndex = 0;
666
+ return pattern.test(v);
667
+ }, "invalid_string", message, { validation: "regex" }, { pattern: pattern.source });
668
+ }
669
+
670
+ /**
671
+ * Starts with `prefix`.
672
+ * @param {string} prefix
673
+ * @param {Message} [message]
674
+ * @returns {this}
675
+ */
676
+ startsWith(prefix, message = `Must start with "${prefix}"`) {
677
+ return this._rule((v) => v.startsWith(prefix), "invalid_string", message);
678
+ }
679
+
680
+ /**
681
+ * Ends with `suffix`.
682
+ * @param {string} suffix
683
+ * @param {Message} [message]
684
+ * @returns {this}
685
+ */
686
+ endsWith(suffix, message = `Must end with "${suffix}"`) {
687
+ return this._rule((v) => v.endsWith(suffix), "invalid_string", message);
688
+ }
689
+
690
+ /**
691
+ * Contains `search`.
692
+ * @param {string} search
693
+ * @param {Message} [message]
694
+ * @returns {this}
695
+ */
696
+ includes(search, message = `Must include "${search}"`) {
697
+ return this._rule((v) => v.includes(search), "invalid_string", message);
698
+ }
699
+
700
+ /**
701
+ * Trims whitespace before the checks that follow.
702
+ * @returns {this}
703
+ */
704
+ trim() {
705
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.trim() });
706
+ }
707
+
708
+ /**
709
+ * Lowercases before the checks that follow.
710
+ * @returns {this}
711
+ */
712
+ toLowerCase() {
713
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toLowerCase() });
714
+ }
715
+
716
+ /**
717
+ * Uppercases before the checks that follow.
718
+ * @returns {this}
719
+ */
720
+ toUpperCase() {
721
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toUpperCase() });
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
+ }
801
+ }
802
+
803
+ /**
804
+ * A number schema.
805
+ *
806
+ * @extends {Schema<number>}
807
+ */
808
+ class NumberSchema extends Schema {
809
+ /** @protected */
810
+ _coerce = false;
811
+
812
+ /**
813
+ * @param {unknown} value
814
+ * @param {ParseContext} ctx
815
+ * @returns {unknown}
816
+ * @override
817
+ * @protected
818
+ */
819
+ _check(value, ctx) {
820
+ let v = value;
821
+ if (this._coerce && typeof v === "string" && v.trim() !== "") v = Number(v);
822
+ else if (this._coerce && (typeof v === "boolean" || typeof v === "bigint")) v = Number(v);
823
+ else if (this._coerce && types.isDate(v)) v = v.getTime();
824
+ if (typeof v !== "number" || Number.isNaN(v)) return _typeIssue(ctx, "number", value, this._typeMessage);
825
+ if (!Number.isFinite(v)) {
826
+ _issue(ctx, "not_finite", "Must be a finite number");
827
+ return v;
828
+ }
829
+ return v;
830
+ }
831
+
832
+ /**
833
+ * @param {(v: number) => boolean} test
834
+ * @param {string} code
835
+ * @param {Message} message
836
+ * @param {Record<string, unknown>} [details]
837
+ * @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
838
+ * @returns {this}
839
+ * @protected
840
+ */
841
+ _rule(test, code, message, details, json) {
842
+ return this._with({ kind: "check", test, code, message, details, json });
843
+ }
844
+
845
+ /**
846
+ * At least `value`.
847
+ * @param {number} value
848
+ * @param {Message} [message]
849
+ * @returns {this}
850
+ */
851
+ min(value, message = `Must be greater than or equal to ${value}`) {
852
+ return this._rule((v) => v >= value, "too_small", message, { minimum: value }, { minimum: value });
853
+ }
854
+
855
+ /**
856
+ * At most `value`.
857
+ * @param {number} value
858
+ * @param {Message} [message]
859
+ * @returns {this}
860
+ */
861
+ max(value, message = `Must be less than or equal to ${value}`) {
862
+ return this._rule((v) => v <= value, "too_big", message, { maximum: value }, { maximum: value });
863
+ }
864
+
865
+ /**
866
+ * An integer.
867
+ * @param {Message} [message]
868
+ * @returns {this}
869
+ */
870
+ int(message = "Must be an integer") {
871
+ return this._rule(Number.isInteger, "invalid_type", message, { expected: "integer" }, { type: "integer" });
872
+ }
873
+
874
+ /**
875
+ * Greater than 0.
876
+ * @param {Message} [message]
877
+ * @returns {this}
878
+ */
879
+ positive(message = "Must be positive") {
880
+ return this._rule((v) => v > 0, "too_small", message, { minimum: 0, exclusive: true }, { exclusiveMinimum: 0 });
881
+ }
882
+
883
+ /**
884
+ * 0 or more.
885
+ * @param {Message} [message]
886
+ * @returns {this}
887
+ */
888
+ nonnegative(message = "Must not be negative") {
889
+ return this._rule((v) => v >= 0, "too_small", message, { minimum: 0 }, { minimum: 0 });
890
+ }
891
+
892
+ /**
893
+ * Less than 0.
894
+ * @param {Message} [message]
895
+ * @returns {this}
896
+ */
897
+ negative(message = "Must be negative") {
898
+ return this._rule((v) => v < 0, "too_big", message, { maximum: 0, exclusive: true }, { exclusiveMaximum: 0 });
899
+ }
900
+
901
+ /**
902
+ * A multiple of `step`.
903
+ * @param {number} step
904
+ * @param {Message} [message]
905
+ * @returns {this}
906
+ */
907
+ multipleOf(step, message = `Must be a multiple of ${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" };
948
+ }
949
+ }
950
+
951
+ /**
952
+ * A boolean schema.
953
+ *
954
+ * @extends {Schema<boolean>}
955
+ */
956
+ class BooleanSchema extends Schema {
957
+ /** @protected */
958
+ _coerce = false;
959
+
960
+ /**
961
+ * @param {unknown} value
962
+ * @param {ParseContext} ctx
963
+ * @returns {unknown}
964
+ * @override
965
+ * @protected
966
+ */
967
+ _check(value, ctx) {
968
+ if (typeof value === "boolean") return value;
969
+ if (this._coerce) {
970
+ const text = String(value).trim().toLowerCase();
971
+ if (_TRUE.includes(text)) return true;
972
+ if (_FALSE.includes(text)) return false;
973
+ }
974
+ return _typeIssue(ctx, "boolean", value, this._typeMessage);
975
+ }
976
+
977
+ /**
978
+ * @returns {JSONSchema}
979
+ * @override
980
+ * @protected
981
+ */
982
+ _json() {
983
+ return { type: "boolean" };
984
+ }
985
+ }
986
+
987
+ /**
988
+ * A bigint schema.
989
+ *
990
+ * @extends {Schema<bigint>}
991
+ */
992
+ class BigIntSchema extends Schema {
993
+ /** @protected */
994
+ _coerce = false;
995
+
996
+ /**
997
+ * @param {unknown} value
998
+ * @param {ParseContext} ctx
999
+ * @returns {unknown}
1000
+ * @override
1001
+ * @protected
1002
+ */
1003
+ _check(value, ctx) {
1004
+ if (typeof value === "bigint") return value;
1005
+ if (this._coerce && (typeof value === "string" || typeof value === "number")) {
1006
+ try {
1007
+ return BigInt(value);
1008
+ } catch {
1009
+ // fall through to the error
1010
+ }
1011
+ }
1012
+ return _typeIssue(ctx, "bigint", value, this._typeMessage);
1013
+ }
1014
+
1015
+ /**
1016
+ * @returns {JSONSchema}
1017
+ * @override
1018
+ * @protected
1019
+ */
1020
+ _json() {
1021
+ return { type: "integer" };
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * A date schema.
1027
+ *
1028
+ * @extends {Schema<Date>}
1029
+ */
1030
+ class DateSchema extends Schema {
1031
+ /** @protected */
1032
+ _coerce = false;
1033
+
1034
+ /**
1035
+ * @param {unknown} value
1036
+ * @param {ParseContext} ctx
1037
+ * @returns {unknown}
1038
+ * @override
1039
+ * @protected
1040
+ */
1041
+ _check(value, ctx) {
1042
+ let v = value;
1043
+ if (this._coerce && (typeof v === "string" || typeof v === "number")) v = new Date(v);
1044
+ if (!types.isDate(v)) return _typeIssue(ctx, "date", value, this._typeMessage);
1045
+ if (Number.isNaN(v.getTime())) return _issue(ctx, "invalid_date", "Invalid date");
1046
+ return v;
1047
+ }
1048
+
1049
+ /**
1050
+ * On or after `date`.
1051
+ * @param {Date | number | string} date
1052
+ * @param {Message} [message]
1053
+ * @returns {this}
1054
+ */
1055
+ min(date, message) {
1056
+ const limit = new Date(date).getTime();
1057
+ return this._with({ kind: "check", test: (/** @type {Date} */ v) => v.getTime() >= limit, code: "too_small", message: message ?? `Must be on or after ${new Date(limit).toISOString()}` });
1058
+ }
1059
+
1060
+ /**
1061
+ * On or before `date`.
1062
+ * @param {Date | number | string} date
1063
+ * @param {Message} [message]
1064
+ * @returns {this}
1065
+ */
1066
+ max(date, message) {
1067
+ const limit = new Date(date).getTime();
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()}` });
1069
+ }
1070
+
1071
+ /**
1072
+ * @returns {JSONSchema}
1073
+ * @override
1074
+ * @protected
1075
+ */
1076
+ _json() {
1077
+ return { type: "string", format: "date-time" };
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * A schema that accepts a fixed set of values.
1083
+ *
1084
+ * @template T
1085
+ * @extends {Schema<T>}
1086
+ */
1087
+ class LiteralSchema extends Schema {
1088
+ /** @param {readonly T[]} values */
1089
+ constructor(values) {
1090
+ super();
1091
+ /** @protected */
1092
+ this._values = values;
1093
+ /** @protected @type {Set<unknown>} */
1094
+ this._set = new Set(values);
1095
+ }
1096
+
1097
+ /**
1098
+ * @param {unknown} value
1099
+ * @param {ParseContext} ctx
1100
+ * @returns {unknown}
1101
+ * @override
1102
+ * @protected
1103
+ */
1104
+ _check(value, ctx) {
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;
1107
+ const list = this._values.map((v) => JSON.stringify(v)).join(" | ");
1108
+ 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 ${JSON.stringify(value) ?? String(value)}`, { expected: list, received: _typeName(value) });
1110
+ }
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
+
1122
+ /** The accepted values. */
1123
+ get options() {
1124
+ return [...this._values];
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * A schema built from a test function.
1130
+ *
1131
+ * @template T
1132
+ * @extends {Schema<T>}
1133
+ */
1134
+ class CustomSchema extends Schema {
1135
+ /**
1136
+ * @param {(value: unknown) => boolean} test
1137
+ * @param {Message} message
1138
+ * @param {string} expected
1139
+ */
1140
+ constructor(test, message, expected) {
1141
+ super();
1142
+ /** @protected */
1143
+ this._test = test;
1144
+ /** @protected */
1145
+ this._message = message;
1146
+ /** @protected */
1147
+ this._expected = expected;
1148
+ }
1149
+
1150
+ /**
1151
+ * @param {unknown} value
1152
+ * @param {ParseContext} ctx
1153
+ * @returns {unknown}
1154
+ * @override
1155
+ * @protected
1156
+ */
1157
+ _check(value, ctx) {
1158
+ if (this._test(value)) return value;
1159
+ return _issue(ctx, this._expected === "custom" ? "custom" : "invalid_type", _message(this._message, value), { expected: this._expected, received: _typeName(value) });
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * An array schema.
1165
+ *
1166
+ * @template T
1167
+ * @extends {Schema<T[]>}
1168
+ */
1169
+ class ArraySchema extends Schema {
1170
+ /** @param {Schema<T>} item */
1171
+ constructor(item) {
1172
+ super();
1173
+ /** @protected */
1174
+ this._item = item;
1175
+ }
1176
+
1177
+ /**
1178
+ * @param {unknown} value
1179
+ * @param {ParseContext} ctx
1180
+ * @returns {unknown}
1181
+ * @override
1182
+ * @protected
1183
+ */
1184
+ _check(value, ctx) {
1185
+ if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
1186
+ const item = this._item;
1187
+ const path = ctx.path;
1188
+ const out = new Array(value.length);
1189
+ for (let i = 0; i < value.length; i++) {
1190
+ path.push(i);
1191
+ out[i] = _runChild(item, value[i], ctx);
1192
+ path.pop();
1193
+ }
1194
+ return out;
1195
+ }
1196
+
1197
+ /**
1198
+ * @param {(v: T[]) => boolean} test
1199
+ * @param {string} code
1200
+ * @param {Message} message
1201
+ * @param {Record<string, unknown>} [details]
1202
+ * @param {Record<string, unknown>} [json]
1203
+ * @returns {this}
1204
+ * @protected
1205
+ */
1206
+ _rule(test, code, message, details, json) {
1207
+ return this._with({ kind: "check", test, code, message, details, json });
1208
+ }
1209
+
1210
+ /**
1211
+ * At least `count` items.
1212
+ * @param {number} count
1213
+ * @param {Message} [message]
1214
+ * @returns {this}
1215
+ */
1216
+ min(count, message = `Must contain at least ${count} item${count > 1 ? "s" : ""}`) {
1217
+ return this._rule((v) => v.length >= count, "too_small", message, { minimum: count }, { minItems: count });
1218
+ }
1219
+
1220
+ /**
1221
+ * At most `count` items.
1222
+ * @param {number} count
1223
+ * @param {Message} [message]
1224
+ * @returns {this}
1225
+ */
1226
+ max(count, message = `Must contain at most ${count} item${count > 1 ? "s" : ""}`) {
1227
+ return this._rule((v) => v.length <= count, "too_big", message, { maximum: count }, { maxItems: count });
1228
+ }
1229
+
1230
+ /**
1231
+ * Exactly `count` items.
1232
+ * @param {number} count
1233
+ * @param {Message} [message]
1234
+ * @returns {this}
1235
+ */
1236
+ length(count, message = `Must contain exactly ${count} items`) {
1237
+ return this._rule((v) => v.length === count, "invalid_length", message, { exact: count }, { minItems: count, maxItems: count });
1238
+ }
1239
+
1240
+ /**
1241
+ * At least one item.
1242
+ * @param {Message} [message]
1243
+ * @returns {this}
1244
+ */
1245
+ nonempty(message = "Must not be empty") {
1246
+ return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minItems: 1 });
1247
+ }
1248
+
1249
+ /**
1250
+ * No duplicates, compared by value or by a key.
1251
+ * @param {((item: T) => unknown) | Message} [keyOrMessage] A function returning the key, or a message.
1252
+ * @param {Message} [message]
1253
+ * @returns {this}
1254
+ */
1255
+ unique(keyOrMessage, message = "Must not contain duplicates") {
1256
+ const byKey = typeof keyOrMessage === "function" && keyOrMessage.length === 1 ? /** @type {(item: T) => unknown} */ (keyOrMessage) : undefined;
1257
+ const msg = typeof keyOrMessage === "string" ? keyOrMessage : message;
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) };
1269
+ }
1270
+
1271
+ /** The schema of the items. */
1272
+ get element() {
1273
+ return this._item;
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * An object schema.
1279
+ *
1280
+ * @template {Shape} S
1281
+ * @extends {Schema<ObjectOutput<S>>}
1282
+ */
1283
+ class ObjectSchema extends Schema {
1284
+ /** @protected @type {"strip" | "strict" | "passthrough"} */
1285
+ _unknown = "strip";
1286
+
1287
+ /** @param {S} shape */
1288
+ constructor(shape) {
1289
+ super();
1290
+ /** @protected */
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]);
1298
+ }
1299
+
1300
+ /**
1301
+ * @param {unknown} value
1302
+ * @param {ParseContext} ctx
1303
+ * @returns {unknown}
1304
+ * @override
1305
+ * @protected
1306
+ */
1307
+ _check(value, ctx) {
1308
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
1309
+ const input = /** @type {Record<string, unknown>} */ (value);
1310
+ const keys = this._keys;
1311
+ const schemas = this._schemas;
1312
+ const path = ctx.path;
1313
+ /** @type {Record<string, unknown>} */
1314
+ const out = {};
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;
1324
+ }
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];
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;
1369
+ return out;
1370
+ }
1371
+
1372
+ /** The schema of each key. */
1373
+ get shape() {
1374
+ return this._shape;
1375
+ }
1376
+
1377
+ /**
1378
+ * Rejects unknown keys instead of dropping them.
1379
+ * @returns {this}
1380
+ */
1381
+ strict() {
1382
+ const copy = this._clone();
1383
+ copy._unknown = "strict";
1384
+ return copy;
1385
+ }
1386
+
1387
+ /**
1388
+ * Keeps unknown keys instead of dropping them.
1389
+ * @returns {this}
1390
+ */
1391
+ passthrough() {
1392
+ const copy = this._clone();
1393
+ copy._unknown = "passthrough";
1394
+ return copy;
1395
+ }
1396
+
1397
+ /**
1398
+ * Adds keys, or replaces existing ones.
1399
+ * @template {Shape} M
1400
+ * @param {M} more
1401
+ * @returns {ObjectSchema<Omit<S, keyof M> & M>}
1402
+ */
1403
+ extend(more) {
1404
+ return this._reshape({ ...this._shape, ...more });
1405
+ }
1406
+
1407
+ /**
1408
+ * Keeps only some keys.
1409
+ * @template {keyof S & string} K
1410
+ * @param {readonly K[]} keys
1411
+ * @returns {ObjectSchema<Pick<S, K>>}
1412
+ */
1413
+ pick(keys) {
1414
+ /** @type {any} */
1415
+ const shape = {};
1416
+ for (const key of keys) shape[key] = this._shape[key];
1417
+ return this._reshape(shape);
1418
+ }
1419
+
1420
+ /**
1421
+ * Drops some keys.
1422
+ * @template {keyof S & string} K
1423
+ * @param {readonly K[]} keys
1424
+ * @returns {ObjectSchema<Omit<S, K>>}
1425
+ */
1426
+ omit(keys) {
1427
+ /** @type {any} */
1428
+ const shape = { ...this._shape };
1429
+ for (const key of keys) delete shape[key];
1430
+ return this._reshape(shape);
1431
+ }
1432
+
1433
+ /**
1434
+ * Makes every key optional, which suits PATCH requests.
1435
+ * @returns {ObjectSchema<{ [K in keyof S]: Schema<Infer<S[K]> | undefined> }>}
1436
+ */
1437
+ partial() {
1438
+ /** @type {any} */
1439
+ const shape = {};
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));
1488
+ }
1489
+ }
1490
+
1491
+ /**
1492
+ * A schema that accepts any of several schemas.
1493
+ *
1494
+ * @template T
1495
+ * @extends {Schema<T>}
1496
+ */
1497
+ class UnionSchema extends Schema {
1498
+ /** @param {ReadonlyArray<Schema<any>>} options */
1499
+ constructor(options) {
1500
+ super();
1501
+ /** @protected */
1502
+ this._options = options;
1503
+ }
1504
+
1505
+ /**
1506
+ * @param {unknown} value
1507
+ * @param {ParseContext} ctx
1508
+ * @returns {unknown}
1509
+ * @override
1510
+ * @protected
1511
+ */
1512
+ _check(value, ctx) {
1513
+ /** @type {import("./errors").ValidationIssue[][]} */
1514
+ const failures = [];
1515
+ for (const option of this._options) {
1516
+ const attempt = { issues: [], path: ctx.path };
1517
+ const out = _runChild(option, value, attempt);
1518
+ if (!attempt.issues.length) return out;
1519
+ failures.push(attempt.issues);
1520
+ }
1521
+ const typeOnly = failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === ctx.path.length);
1522
+ if (typeOnly) {
1523
+ const expected = failures.map((issues) => issues[0].expected).join(" | ");
1524
+ return _issue(ctx, "invalid_union", `Expected ${expected}, received ${_typeName(value)}`, { expected, received: _typeName(value) });
1525
+ }
1526
+ const best = failures.reduce((a, b) => (b.length < a.length ? b : a));
1527
+ ctx.issues.push(...best);
1528
+ return INVALID;
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
+ }
1599
+ }
1600
+
1601
+ /**
1602
+ * A fixed-length array schema.
1603
+ *
1604
+ * @template {any[]} T
1605
+ * @extends {Schema<T>}
1606
+ */
1607
+ class TupleSchema extends Schema {
1608
+ /** @param {ReadonlyArray<Schema<any>>} items */
1609
+ constructor(items) {
1610
+ super();
1611
+ /** @protected */
1612
+ this._items = items;
1613
+ }
1614
+
1615
+ /**
1616
+ * @param {unknown} value
1617
+ * @param {ParseContext} ctx
1618
+ * @returns {unknown}
1619
+ * @override
1620
+ * @protected
1621
+ */
1622
+ _check(value, ctx) {
1623
+ if (!Array.isArray(value)) return _typeIssue(ctx, "tuple", value, this._typeMessage);
1624
+ if (value.length !== this._items.length) {
1625
+ return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
1626
+ }
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 };
1645
+ }
1646
+ }
1647
+
1648
+ /**
1649
+ * A dictionary schema.
1650
+ *
1651
+ * @template V
1652
+ * @extends {Schema<Record<string, V>>}
1653
+ */
1654
+ class RecordSchema extends Schema {
1655
+ /**
1656
+ * @param {Schema<V>} values
1657
+ * @param {Schema<string> | undefined} keys
1658
+ */
1659
+ constructor(values, keys) {
1660
+ super();
1661
+ /** @protected */
1662
+ this._values = values;
1663
+ /** @protected */
1664
+ this._keys = keys;
1665
+ }
1666
+
1667
+ /**
1668
+ * @param {unknown} value
1669
+ * @param {ParseContext} ctx
1670
+ * @returns {unknown}
1671
+ * @override
1672
+ * @protected
1673
+ */
1674
+ _check(value, ctx) {
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;
1678
+ /** @type {Record<string, unknown>} */
1679
+ const out = {};
1680
+ for (const key of Object.keys(input)) {
1681
+ if (UNSAFE_KEYS.has(key)) continue;
1682
+ path.push(key);
1683
+ const k = this._keys ? _runChild(this._keys, key, ctx) : key;
1684
+ const v = _runChild(this._values, input[key], ctx);
1685
+ path.pop();
1686
+ if (typeof k === "string" && !UNSAFE_KEYS.has(k)) out[k] = v;
1687
+ }
1688
+ return out;
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
+ }
1703
+ }
1704
+
1705
+ /**
1706
+ * A schema resolved on first use, for recursive data.
1707
+ *
1708
+ * @template T
1709
+ * @extends {Schema<T>}
1710
+ */
1711
+ class LazySchema extends Schema {
1712
+ /** @param {() => Schema<T>} getter */
1713
+ constructor(getter) {
1714
+ super();
1715
+ /** @protected */
1716
+ this._getter = getter;
1717
+ /** @protected @type {Schema<T> | undefined} */
1718
+ this._resolved = undefined;
1719
+ }
1720
+
1721
+ /**
1722
+ * @param {unknown} value
1723
+ * @param {ParseContext} ctx
1724
+ * @returns {unknown}
1725
+ * @override
1726
+ * @protected
1727
+ */
1728
+ _check(value, ctx) {
1729
+ const before = ctx.issues.length;
1730
+ const out = _runChild((this._resolved ??= this._getter()), value, ctx);
1731
+ return ctx.issues.length > before ? INVALID : out;
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
+ }
1752
+ }
1753
+
1754
+ /**
1755
+ * A string schema.
1756
+ *
1757
+ * @example
1758
+ * s.string().trim().min(1).max(100);
1759
+ * s.string().email();
1760
+ *
1761
+ * @param {{ message?: Message }} [options] Message when the value isn't a string.
1762
+ * @returns {StringSchema}
1763
+ */
1764
+ function string(options = {}) {
1765
+ const schema = new StringSchema();
1766
+ /** @type {any} */ (schema)._typeMessage = options.message;
1767
+ return schema;
1768
+ }
1769
+
1770
+ /**
1771
+ * A number. `NaN` and `Infinity` are refused.
1772
+ *
1773
+ * @example
1774
+ * s.number().int().min(0).max(120);
1775
+ *
1776
+ * @param {{ message?: Message }} [options] Message when the value isn't a number.
1777
+ * @returns {NumberSchema}
1778
+ */
1779
+ function number(options = {}) {
1780
+ const schema = new NumberSchema();
1781
+ /** @type {any} */ (schema)._typeMessage = options.message;
1782
+ return schema;
1783
+ }
1784
+
1785
+ /**
1786
+ * A boolean schema.
1787
+ *
1788
+ * @param {{ message?: Message }} [options]
1789
+ * @returns {BooleanSchema}
1790
+ */
1791
+ function boolean(options = {}) {
1792
+ const schema = new BooleanSchema();
1793
+ /** @type {any} */ (schema)._typeMessage = options.message;
1794
+ return schema;
1795
+ }
1796
+
1797
+ /**
1798
+ * A bigint schema.
1799
+ *
1800
+ * @param {{ message?: Message }} [options]
1801
+ * @returns {BigIntSchema}
1802
+ */
1803
+ function bigint(options = {}) {
1804
+ const schema = new BigIntSchema();
1805
+ /** @type {any} */ (schema)._typeMessage = options.message;
1806
+ return schema;
1807
+ }
1808
+
1809
+ /**
1810
+ * A valid `Date`.
1811
+ *
1812
+ * @example
1813
+ * s.date().min("2024-01-01");
1814
+ *
1815
+ * @param {{ message?: Message }} [options]
1816
+ * @returns {DateSchema}
1817
+ */
1818
+ function date(options = {}) {
1819
+ const schema = new DateSchema();
1820
+ /** @type {any} */ (schema)._typeMessage = options.message;
1821
+ return schema;
1822
+ }
1823
+
1824
+ /**
1825
+ * Exactly this value.
1826
+ *
1827
+ * @example
1828
+ * s.object({ type: s.literal("circle"), radius: s.number() });
1829
+ *
1830
+ * @template {string | number | boolean | bigint | null | undefined} L
1831
+ * @param {L} value
1832
+ * @returns {LiteralSchema<L>}
1833
+ */
1834
+ function literal(value) {
1835
+ return new LiteralSchema([value]);
1836
+ }
1837
+
1838
+ /**
1839
+ * One of these values. The type is their union.
1840
+ *
1841
+ * @example
1842
+ * const Role = s.enum(["admin", "editor", "viewer"]); // Schema<"admin" | "editor" | "viewer">
1843
+ * Role.options; // ["admin", "editor", "viewer"]
1844
+ *
1845
+ * @template {string | number} V
1846
+ * @param {readonly V[]} values
1847
+ * @param {{ message?: Message }} [options]
1848
+ * @returns {LiteralSchema<V>}
1849
+ */
1850
+ function enumeration(values, options = {}) {
1851
+ const schema = new LiteralSchema(values);
1852
+ /** @type {any} */ (schema)._typeMessage = options.message;
1853
+ return schema;
1854
+ }
1855
+
1856
+ /**
1857
+ * An array whose items all match `item`.
1858
+ *
1859
+ * @example
1860
+ * s.array(s.string()).min(1).max(10).unique();
1861
+ *
1862
+ * @template T
1863
+ * @param {Schema<T>} item
1864
+ * @returns {ArraySchema<T>}
1865
+ */
1866
+ function array(item) {
1867
+ return new ArraySchema(item);
1868
+ }
1869
+
1870
+ /**
1871
+ * An object with known keys. Unknown keys are dropped; `.strict()` rejects
1872
+ * them and `.passthrough()` keeps them.
1873
+ *
1874
+ * @example
1875
+ * const Address = s.object({ street: s.string(), zip: s.string().regex(/^\d{5}$/), city: s.string() });
1876
+ *
1877
+ * @template {Shape} S
1878
+ * @param {S} shape
1879
+ * @returns {ObjectSchema<S>}
1880
+ */
1881
+ function object(shape) {
1882
+ return new ObjectSchema(shape);
1883
+ }
1884
+
1885
+ /**
1886
+ * Matches any of these schemas; the first that fits wins.
1887
+ *
1888
+ * @example
1889
+ * s.union([s.string(), s.number()]); // Schema<string | number>
1890
+ *
1891
+ * @template {ReadonlyArray<Schema<any>>} U
1892
+ * @param {U} options
1893
+ * @returns {UnionSchema<Infer<U[number]>>}
1894
+ */
1895
+ function union(options) {
1896
+ return new UnionSchema(options);
1897
+ }
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
+
1920
+ /**
1921
+ * A fixed-length array with one schema per position.
1922
+ *
1923
+ * @example
1924
+ * s.tuple([s.number(), s.number()]); // Schema<[number, number]>
1925
+ *
1926
+ * @template {[Schema<any>, ...Schema<any>[]] | []} T
1927
+ * @param {T} items
1928
+ * @returns {TupleSchema<{ -readonly [K in keyof T]: Infer<T[K]> }>}
1929
+ */
1930
+ function tuple(items) {
1931
+ return /** @type {any} */ (new TupleSchema(items));
1932
+ }
1933
+
1934
+ /**
1935
+ * An object with any keys whose values match a schema, like a dictionary.
1936
+ *
1937
+ * @example
1938
+ * s.record(s.number()); // Record<string, number>
1939
+ * s.record(s.boolean(), s.string().regex(/^[a-z]+$/)); // keys checked too
1940
+ *
1941
+ * @template V
1942
+ * @param {Schema<V>} values
1943
+ * @param {Schema<string>} [keys]
1944
+ * @returns {RecordSchema<V>}
1945
+ */
1946
+ function record(values, keys) {
1947
+ return new RecordSchema(values, keys);
1948
+ }
1949
+
1950
+ /**
1951
+ * A schema defined later, for recursive data like trees.
1952
+ *
1953
+ * @example
1954
+ * const Category = s.object({
1955
+ * name: s.string(),
1956
+ * children: s.lazy(() => s.array(Category)).default([]),
1957
+ * });
1958
+ *
1959
+ * @template T
1960
+ * @param {() => Schema<T>} getter
1961
+ * @returns {LazySchema<T>}
1962
+ */
1963
+ function lazy(getter) {
1964
+ return new LazySchema(getter);
1965
+ }
1966
+
1967
+ /**
1968
+ * An instance of a class.
1969
+ *
1970
+ * @example
1971
+ * s.instanceOf(URL);
1972
+ *
1973
+ * @template {new (...args: any[]) => any} C
1974
+ * @param {C} ctor
1975
+ * @param {Message} [message]
1976
+ * @returns {CustomSchema<InstanceType<C>>}
1977
+ */
1978
+ function instanceOf(ctor, message = `Expected an instance of ${ctor.name}`) {
1979
+ return new CustomSchema((value) => value instanceof ctor, message, ctor.name);
1980
+ }
1981
+
1982
+ /**
1983
+ * A schema from any test, usually a type guard.
1984
+ *
1985
+ * @example
1986
+ * const Color = s.custom(nc.isHexColor, "Expected a hex color");
1987
+ *
1988
+ * @template [T=unknown]
1989
+ * @param {((value: unknown) => value is T) | ((value: unknown) => boolean)} test
1990
+ * @param {Message} [message]
1991
+ * @returns {CustomSchema<T>}
1992
+ */
1993
+ function custom(test, message = "Invalid value") {
1994
+ return new CustomSchema(test, message, "custom");
1995
+ }
1996
+
1997
+ /**
1998
+ * Anything, typed as `any`.
1999
+ * @returns {Schema<any>}
2000
+ */
2001
+ function any() {
2002
+ return new Schema();
2003
+ }
2004
+
2005
+ /**
2006
+ * Anything, typed as `unknown`.
2007
+ * @returns {Schema<unknown>}
2008
+ */
2009
+ function unknown() {
2010
+ return new Schema();
2011
+ }
2012
+
2013
+ /**
2014
+ * The `s.coerce` builders.
2015
+ * @typedef {object} CoerceBuilders
2016
+ * @property {() => StringSchema} string A string schema that turns any value into a string first.
2017
+ * @property {() => NumberSchema} number A number schema that converts strings like `"42"`, booleans and dates first.
2018
+ * @property {() => BooleanSchema} boolean A boolean schema that understands `"true"`, `"1"`, `"yes"`, `"on"` and their opposites.
2019
+ * @property {() => DateSchema} date A date schema that converts ISO strings and timestamps.
2020
+ * @property {() => BigIntSchema} bigint A bigint schema that converts numeric strings and integers.
2021
+ */
2022
+
2023
+ /**
2024
+ * Schemas that convert their input before checking it. Query strings, form
2025
+ * fields and environment variables are always strings, so this is what you
2026
+ * want for them.
2027
+ *
2028
+ * @example
2029
+ * const Query = s.object({
2030
+ * page: s.coerce.number().int().min(1).default(1),
2031
+ * active: s.coerce.boolean().default(false),
2032
+ * });
2033
+ * Query.parse({ page: "2", active: "true" }); // { page: 2, active: true }
2034
+ *
2035
+ * @type {CoerceBuilders}
2036
+ */
2037
+ const coerce = {
2038
+ /**
2039
+ * @returns {StringSchema}
2040
+ */
2041
+ string() {
2042
+ const schema = new StringSchema();
2043
+ /** @type {any} */ (schema)._coerce = true;
2044
+ return schema;
2045
+ },
2046
+ /**
2047
+ * @returns {NumberSchema}
2048
+ */
2049
+ number() {
2050
+ const schema = new NumberSchema();
2051
+ /** @type {any} */ (schema)._coerce = true;
2052
+ return schema;
2053
+ },
2054
+ /**
2055
+ * @returns {BooleanSchema}
2056
+ */
2057
+ boolean() {
2058
+ const schema = new BooleanSchema();
2059
+ /** @type {any} */ (schema)._coerce = true;
2060
+ return schema;
2061
+ },
2062
+ /**
2063
+ * @returns {DateSchema}
2064
+ */
2065
+ date() {
2066
+ const schema = new DateSchema();
2067
+ /** @type {any} */ (schema)._coerce = true;
2068
+ return schema;
2069
+ },
2070
+ /**
2071
+ * @returns {BigIntSchema}
2072
+ */
2073
+ bigint() {
2074
+ const schema = new BigIntSchema();
2075
+ /** @type {any} */ (schema)._coerce = true;
2076
+ return schema;
2077
+ },
2078
+ };
2079
+
2080
+ module.exports = {
2081
+ string,
2082
+ number,
2083
+ boolean,
2084
+ bigint,
2085
+ date,
2086
+ literal,
2087
+ enum: enumeration,
2088
+ array,
2089
+ object,
2090
+ union,
2091
+ discriminatedUnion,
2092
+ tuple,
2093
+ record,
2094
+ lazy,
2095
+ instanceOf,
2096
+ custom,
2097
+ any,
2098
+ unknown,
2099
+ coerce,
2100
+ Schema,
2101
+ };