@ix-xs/node-comfort 1.2.0 → 2.0.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 (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. package/types/index.d.ts.map +0 -1
package/src/Schema.js ADDED
@@ -0,0 +1,1586 @@
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 }
19
+ */
20
+
21
+ const { ValidationError } = require("./errors.js");
22
+ const { types } = require("node:util");
23
+
24
+ /**
25
+ * The type a schema produces.
26
+ *
27
+ * @example
28
+ * // type User = Infer<typeof UserSchema>;
29
+ *
30
+ * @template S
31
+ * @typedef {S extends Schema<infer T> ? T : never} Infer
32
+ */
33
+
34
+ /**
35
+ * An object of schemas, as passed to `object()`.
36
+ * @typedef {Record<string, Schema<any>>} Shape
37
+ */
38
+
39
+ /**
40
+ * Flattens a type so editors show it nicely.
41
+ * @template T
42
+ * @typedef {{ [K in keyof T]: T[K] } & {}} Simplify
43
+ */
44
+
45
+ /**
46
+ * The type of an object schema. Keys that accept `undefined` become optional.
47
+ * @template {Shape} S
48
+ * @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
49
+ */
50
+
51
+ /**
52
+ * What `safeParse()` returns.
53
+ * @template T
54
+ * @typedef {{ success: true, data: T, error?: undefined } | { success: false, data?: undefined, error: ValidationError }} SafeParseResult
55
+ */
56
+
57
+ /**
58
+ * An error message: a string, or a function that gets the invalid value.
59
+ * @typedef {string | ((value: any) => string)} Message
60
+ */
61
+
62
+ /**
63
+ * Options for `refine()`.
64
+ * @typedef {object} RefineOptions
65
+ * @property {Message} [message] Defaults to `"Invalid value"`.
66
+ * @property {Array<string | number>} [path] Where to report the issue, like `["confirmPassword"]` for a cross-field check.
67
+ * @property {string} [code] Defaults to `"custom"`.
68
+ */
69
+
70
+ /**
71
+ * @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number> }} ParseContext
72
+ */
73
+
74
+ /**
75
+ * A step in a schema: a check, a transformation or a refinement.
76
+ * @typedef {{ kind: "check", test: (value: any) => boolean, code: string, message: Message, details?: Record<string, unknown> } | { kind: "transform", fn: (value: any) => any } | { kind: "refine", test: (value: any) => boolean, options: RefineOptions }} Step
77
+ */
78
+
79
+ const INVALID = Symbol("invalid");
80
+
81
+ /** @param {unknown} value */
82
+ const _typeName = (value) => {
83
+ if (value === null) return "null";
84
+ if (Array.isArray(value)) return "array";
85
+ if (types.isDate(value)) return Number.isNaN(value.getTime()) ? "invalid date" : "date";
86
+ if (typeof value === "number" && Number.isNaN(value)) return "NaN";
87
+ return typeof value;
88
+ };
89
+
90
+ /**
91
+ * @param {Message} message
92
+ * @param {unknown} value
93
+ */
94
+ const _message = (message, value) => (typeof message === "function" ? message(value) : message);
95
+
96
+ /**
97
+ * @param {ParseContext} ctx
98
+ * @param {string} code
99
+ * @param {string} message
100
+ * @param {Record<string, unknown>} [details]
101
+ */
102
+ const _issue = (ctx, code, message, details) => {
103
+ ctx.issues.push({ path: [...ctx.path], code, message, ...details });
104
+ };
105
+
106
+ /**
107
+ * @param {ParseContext} ctx
108
+ * @param {string} expected
109
+ * @param {unknown} value
110
+ * @param {Message} [message]
111
+ */
112
+ const _typeIssue = (ctx, expected, value, message) => {
113
+ const received = _typeName(value);
114
+ /** @protected */
115
+ _issue(ctx, "invalid_type", message ? _message(message, value) : value === undefined ? "Required" : `Expected ${expected}, received ${received}`, { expected, received });
116
+ };
117
+
118
+ /**
119
+ * Runs a nested schema.
120
+ * @param {Schema<any>} schema
121
+ * @param {unknown} value
122
+ * @param {ParseContext} ctx
123
+ * @returns {unknown}
124
+ */
125
+ const _runChild = (schema, value, ctx) => /** @type {any} */ (schema)._run(value, ctx);
126
+
127
+ /**
128
+ * The base of every schema.
129
+ *
130
+ * @template [T=unknown]
131
+ */
132
+ class Schema {
133
+ /** @protected @type {Step[]} */
134
+ _steps = [];
135
+ /** @protected */
136
+ _optional = false;
137
+ /** @protected */
138
+ _nullable = false;
139
+ /** @protected @type {{ value: unknown } | undefined} */
140
+ _default = undefined;
141
+ /** @protected @type {{ value: unknown } | undefined} */
142
+ _catch = undefined;
143
+ /** @protected @type {Message | undefined} */
144
+ _typeMessage = undefined;
145
+ /** @protected @type {string | undefined} */
146
+ _description = undefined;
147
+
148
+ /**
149
+ * Checks the type, and coerces when asked. Subclasses override it.
150
+ * @param {unknown} value
151
+ * @param {ParseContext} _ctx
152
+ * @returns {unknown}
153
+ * @protected
154
+ */
155
+ _check(value, _ctx) {
156
+ return value;
157
+ }
158
+
159
+ /**
160
+ * @returns {this}
161
+ * @protected
162
+ */
163
+ _clone() {
164
+ const copy = Object.create(Object.getPrototypeOf(this));
165
+ Object.assign(copy, this);
166
+ copy._steps = [...this._steps];
167
+ return copy;
168
+ }
169
+
170
+ /**
171
+ * @param {Step} step
172
+ * @returns {this}
173
+ * @protected
174
+ */
175
+ _with(step) {
176
+ const copy = this._clone();
177
+ copy._steps.push(step);
178
+ return copy;
179
+ }
180
+
181
+ /**
182
+ * @param {unknown} input
183
+ * @param {ParseContext} ctx
184
+ * @returns {unknown} The output, or `INVALID`.
185
+ * @protected
186
+ */
187
+ _run(input, ctx) {
188
+ let value = input;
189
+ if (value === undefined && this._default) {
190
+ const def = this._default.value;
191
+ value = typeof def === "function" ? def() : structuredClone(def);
192
+ }
193
+ if (value === undefined && this._optional) return undefined;
194
+ if (value === null && this._nullable) return null;
195
+ const before = ctx.issues.length;
196
+ let out = this._check(value, ctx);
197
+ if (ctx.issues.length === before) {
198
+ for (const step of this._steps) {
199
+ if (step.kind === "transform") {
200
+ if (ctx.issues.length > before) break;
201
+ out = step.fn(out);
202
+ } else if (step.kind === "check") {
203
+ if (!step.test(out)) _issue(ctx, step.code, _message(step.message, out), step.details);
204
+ } else if (ctx.issues.length === before && !step.test(out)) {
205
+ const saved = ctx.path;
206
+ ctx.path = [...ctx.path, ...(step.options.path ?? [])];
207
+ _issue(ctx, step.options.code ?? "custom", _message(step.options.message ?? "Invalid value", out));
208
+ ctx.path = saved;
209
+ }
210
+ }
211
+ }
212
+ if (ctx.issues.length > before) {
213
+ if (this._catch) {
214
+ ctx.issues.length = before;
215
+ const fallback = this._catch.value;
216
+ return typeof fallback === "function" ? fallback(input) : fallback;
217
+ }
218
+ return INVALID;
219
+ }
220
+ return out;
221
+ }
222
+
223
+ /**
224
+ * Validates a value and returns it, with defaults and transformations
225
+ * applied. Unknown object keys are dropped.
226
+ *
227
+ * @example
228
+ * const port = s.coerce.number().int().min(1).max(65535).parse(process.env.PORT);
229
+ *
230
+ * @param {unknown} value
231
+ * @returns {T}
232
+ * @throws {ValidationError} Listing every issue with its path.
233
+ */
234
+ parse(value) {
235
+ const ctx = { issues: [], path: [] };
236
+ const out = this._run(value, ctx);
237
+ if (ctx.issues.length) throw new ValidationError(ctx.issues);
238
+ return /** @type {T} */ (out);
239
+ }
240
+
241
+ /**
242
+ * Validates a value without throwing.
243
+ *
244
+ * @example
245
+ * const result = User.safeParse(req.body);
246
+ * if (!result.success) return res.status(400).json(result.error.flatten());
247
+ * saveUser(result.data);
248
+ *
249
+ * @param {unknown} value
250
+ * @returns {SafeParseResult<T>}
251
+ */
252
+ safeParse(value) {
253
+ const ctx = { issues: [], path: [] };
254
+ const out = this._run(value, ctx);
255
+ if (ctx.issues.length) return { success: false, error: new ValidationError(ctx.issues) };
256
+ return { success: true, data: /** @type {T} */ (out) };
257
+ }
258
+
259
+ /**
260
+ * Is the value valid? Works as a type guard. Transformations don't run.
261
+ *
262
+ * @example
263
+ * if (Email.is(input)) sendMail(input); // input is a string here
264
+ *
265
+ * @param {unknown} value
266
+ * @returns {value is T}
267
+ */
268
+ is(value) {
269
+ return this.safeParse(value).success;
270
+ }
271
+
272
+ /**
273
+ * Also accepts `undefined`. In an object, the key becomes optional.
274
+ *
275
+ * @example
276
+ * s.object({ nickname: s.string().optional() }); // { nickname?: string }
277
+ *
278
+ * @returns {Schema<T | undefined>}
279
+ */
280
+ optional() {
281
+ const copy = this._clone();
282
+ copy._optional = true;
283
+ return copy;
284
+ }
285
+
286
+ /**
287
+ * Also accepts `null`.
288
+ *
289
+ * @returns {Schema<T | null>}
290
+ */
291
+ nullable() {
292
+ const copy = this._clone();
293
+ copy._nullable = true;
294
+ return copy;
295
+ }
296
+
297
+ /**
298
+ * Also accepts `null` and `undefined`.
299
+ *
300
+ * @returns {Schema<T | null | undefined>}
301
+ */
302
+ nullish() {
303
+ const copy = this._clone();
304
+ copy._nullable = true;
305
+ copy._optional = true;
306
+ return copy;
307
+ }
308
+
309
+ /**
310
+ * Uses this value when the input is `undefined`. Objects are copied each
311
+ * time; pass a function to build a fresh value on every parse.
312
+ *
313
+ * @example
314
+ * s.string().default("guest");
315
+ * s.date().default(() => new Date());
316
+ *
317
+ * @param {Exclude<T, undefined> | (() => Exclude<T, undefined>)} value
318
+ * @returns {Schema<Exclude<T, undefined>>}
319
+ */
320
+ default(value) {
321
+ const copy = this._clone();
322
+ copy._default = { value };
323
+ return /** @type {any} */ (copy);
324
+ }
325
+
326
+ /**
327
+ * Returns this value instead of failing when the input is invalid.
328
+ *
329
+ * @example
330
+ * s.number().catch(0).parse("not a number"); // 0
331
+ *
332
+ * @param {T | ((input: unknown) => T)} value A value, or a function that gets the invalid input.
333
+ * @returns {Schema<T>}
334
+ */
335
+ catch(value) {
336
+ const copy = this._clone();
337
+ copy._catch = { value };
338
+ return /** @type {any} */ (copy);
339
+ }
340
+
341
+ /**
342
+ * Changes the value once it's valid. Runs after the checks written before it.
343
+ *
344
+ * @example
345
+ * s.string().transform((value) => value.split(",")); // Schema<string[]>
346
+ *
347
+ * @template U
348
+ * @param {(value: T) => U} fn
349
+ * @returns {Schema<U>}
350
+ */
351
+ transform(fn) {
352
+ return /** @type {any} */ (this._with({ kind: "transform", fn }));
353
+ }
354
+
355
+ /**
356
+ * Adds your own rule.
357
+ *
358
+ * @example
359
+ * s.object({ password: s.string(), confirm: s.string() })
360
+ * .refine((v) => v.password === v.confirm, { message: "Passwords don't match", path: ["confirm"] });
361
+ *
362
+ * @param {(value: T) => boolean} test Returns `true` when the value is fine.
363
+ * @param {Message | RefineOptions} [options]
364
+ * @returns {this}
365
+ */
366
+ refine(test, options = {}) {
367
+ const opts = typeof options === "string" || typeof options === "function" ? { message: options } : options;
368
+ return this._with({ kind: "refine", test, options: opts });
369
+ }
370
+
371
+ /**
372
+ * Accepts values that match this schema or another one.
373
+ *
374
+ * @example
375
+ * s.string().or(s.number()); // Schema<string | number>
376
+ *
377
+ * @template U
378
+ * @param {Schema<U>} other
379
+ * @returns {Schema<T | U>}
380
+ */
381
+ or(other) {
382
+ return union([/** @type {Schema<T>} */ (this), other]);
383
+ }
384
+
385
+ /**
386
+ * An array of this schema.
387
+ *
388
+ * @example
389
+ * s.string().email().array(); // Schema<string[]>
390
+ *
391
+ * @returns {ArraySchema<T>}
392
+ */
393
+ array() {
394
+ return array(/** @type {Schema<T>} */ (this));
395
+ }
396
+
397
+ /**
398
+ * Attaches a description, available as `.description`. Useful to generate
399
+ * docs or forms.
400
+ *
401
+ * @param {string} text
402
+ * @returns {this}
403
+ */
404
+ describe(text) {
405
+ const copy = this._clone();
406
+ copy._description = text;
407
+ return copy;
408
+ }
409
+
410
+ /** The text set with `describe()`. */
411
+ get description() {
412
+ return this._description;
413
+ }
414
+ }
415
+
416
+ /**
417
+ * A string schema.
418
+ *
419
+ * @extends {Schema<string>}
420
+ */
421
+ class StringSchema extends Schema {
422
+ /** @protected */
423
+ _coerce = false;
424
+
425
+ /**
426
+ * @param {unknown} value
427
+ * @param {ParseContext} ctx
428
+ * @returns {unknown}
429
+ * @override
430
+ * @protected
431
+ */
432
+ _check(value, ctx) {
433
+ let v = value;
434
+ if (this._coerce && v !== undefined && v !== null && typeof v !== "string") v = String(v);
435
+ if (typeof v !== "string") return _typeIssue(ctx, "string", value, this._typeMessage);
436
+ return v;
437
+ }
438
+
439
+ /**
440
+ * @param {(v: string) => boolean} test
441
+ * @param {string} code
442
+ * @param {Message} message
443
+ * @param {Record<string, unknown>} [details]
444
+ * @returns {this}
445
+ * @protected
446
+ */
447
+ _rule(test, code, message, details) {
448
+ return this._with({ kind: "check", test, code, message, details });
449
+ }
450
+
451
+ /**
452
+ * At least `length` characters.
453
+ * @param {number} length
454
+ * @param {Message} [message]
455
+ * @returns {this}
456
+ */
457
+ 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 });
459
+ }
460
+
461
+ /**
462
+ * At most `length` characters.
463
+ * @param {number} length
464
+ * @param {Message} [message]
465
+ * @returns {this}
466
+ */
467
+ 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 });
469
+ }
470
+
471
+ /**
472
+ * Exactly `length` characters.
473
+ * @param {number} length
474
+ * @param {Message} [message]
475
+ * @returns {this}
476
+ */
477
+ length(length, message = `Must contain exactly ${length} characters`) {
478
+ return this._rule((v) => v.length === length, "invalid_length", message, { exact: length });
479
+ }
480
+
481
+ /**
482
+ * Not the empty string.
483
+ * @param {Message} [message]
484
+ * @returns {this}
485
+ */
486
+ nonempty(message = "Must not be empty") {
487
+ return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 });
488
+ }
489
+
490
+ /**
491
+ * A valid email address.
492
+ * @param {Message} [message]
493
+ * @returns {this}
494
+ */
495
+ email(message = "Invalid email address") {
496
+ const { isEmail } = require("./Checker.js");
497
+ return this._rule(isEmail, "invalid_string", message, { validation: "email" });
498
+ }
499
+
500
+ /**
501
+ * An absolute URL.
502
+ * @param {Message | { message?: Message, protocols?: string[] }} [options] A message, or allowed protocols (http and https by default).
503
+ * @returns {this}
504
+ */
505
+ url(options = "Invalid URL") {
506
+ const opts = typeof options === "object" ? options : { message: options };
507
+ const { isURL } = require("./Checker.js");
508
+ return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" });
509
+ }
510
+
511
+ /**
512
+ * A UUID.
513
+ * @param {Message} [message]
514
+ * @returns {this}
515
+ */
516
+ uuid(message = "Invalid UUID") {
517
+ const { isUUID } = require("./Checker.js");
518
+ return this._rule((v) => isUUID(v), "invalid_string", message, { validation: "uuid" });
519
+ }
520
+
521
+ /**
522
+ * An IP address.
523
+ * @param {4 | 6 | Message} [versionOrMessage] Only IPv4 or IPv6, or a message.
524
+ * @param {Message} [message]
525
+ * @returns {this}
526
+ */
527
+ ip(versionOrMessage, message = "Invalid IP address") {
528
+ const version = typeof versionOrMessage === "number" ? versionOrMessage : undefined;
529
+ const { isIP } = require("./Checker.js");
530
+ return this._rule((v) => isIP(v, version), "invalid_string", typeof versionOrMessage === "number" ? message : versionOrMessage ?? message, { validation: "ip" });
531
+ }
532
+
533
+ /**
534
+ * An ISO 8601 date or date-time, like `2024-01-31T10:00:00Z`.
535
+ * @param {Message} [message]
536
+ * @returns {this}
537
+ */
538
+ datetime(message = "Invalid ISO date") {
539
+ const { isISODate } = require("./Checker.js");
540
+ return this._rule(isISODate, "invalid_string", message, { validation: "datetime" });
541
+ }
542
+
543
+ /**
544
+ * Matches a regular expression.
545
+ * @param {RegExp} pattern
546
+ * @param {Message} [message]
547
+ * @returns {this}
548
+ */
549
+ regex(pattern, message = `Must match ${pattern}`) {
550
+ return this._rule((v) => {
551
+ pattern.lastIndex = 0;
552
+ return pattern.test(v);
553
+ }, "invalid_string", message, { validation: "regex" });
554
+ }
555
+
556
+ /**
557
+ * Starts with `prefix`.
558
+ * @param {string} prefix
559
+ * @param {Message} [message]
560
+ * @returns {this}
561
+ */
562
+ startsWith(prefix, message = `Must start with "${prefix}"`) {
563
+ return this._rule((v) => v.startsWith(prefix), "invalid_string", message);
564
+ }
565
+
566
+ /**
567
+ * Ends with `suffix`.
568
+ * @param {string} suffix
569
+ * @param {Message} [message]
570
+ * @returns {this}
571
+ */
572
+ endsWith(suffix, message = `Must end with "${suffix}"`) {
573
+ return this._rule((v) => v.endsWith(suffix), "invalid_string", message);
574
+ }
575
+
576
+ /**
577
+ * Contains `search`.
578
+ * @param {string} search
579
+ * @param {Message} [message]
580
+ * @returns {this}
581
+ */
582
+ includes(search, message = `Must include "${search}"`) {
583
+ return this._rule((v) => v.includes(search), "invalid_string", message);
584
+ }
585
+
586
+ /**
587
+ * Trims whitespace before the checks that follow.
588
+ * @returns {this}
589
+ */
590
+ trim() {
591
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.trim() });
592
+ }
593
+
594
+ /**
595
+ * Lowercases before the checks that follow.
596
+ * @returns {this}
597
+ */
598
+ toLowerCase() {
599
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toLowerCase() });
600
+ }
601
+
602
+ /**
603
+ * Uppercases before the checks that follow.
604
+ * @returns {this}
605
+ */
606
+ toUpperCase() {
607
+ return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toUpperCase() });
608
+ }
609
+ }
610
+
611
+ /**
612
+ * A number schema.
613
+ *
614
+ * @extends {Schema<number>}
615
+ */
616
+ class NumberSchema extends Schema {
617
+ /** @protected */
618
+ _coerce = false;
619
+
620
+ /**
621
+ * @param {unknown} value
622
+ * @param {ParseContext} ctx
623
+ * @returns {unknown}
624
+ * @override
625
+ * @protected
626
+ */
627
+ _check(value, ctx) {
628
+ let v = value;
629
+ if (this._coerce && typeof v === "string" && v.trim() !== "") v = Number(v);
630
+ else if (this._coerce && (typeof v === "boolean" || typeof v === "bigint")) v = Number(v);
631
+ else if (this._coerce && types.isDate(v)) v = v.getTime();
632
+ if (typeof v !== "number" || Number.isNaN(v)) return _typeIssue(ctx, "number", value, this._typeMessage);
633
+ if (!Number.isFinite(v)) {
634
+ _issue(ctx, "not_finite", "Must be a finite number");
635
+ return v;
636
+ }
637
+ return v;
638
+ }
639
+
640
+ /**
641
+ * @param {(v: number) => boolean} test
642
+ * @param {string} code
643
+ * @param {Message} message
644
+ * @param {Record<string, unknown>} [details]
645
+ * @returns {this}
646
+ * @protected
647
+ */
648
+ _rule(test, code, message, details) {
649
+ return this._with({ kind: "check", test, code, message, details });
650
+ }
651
+
652
+ /**
653
+ * At least `value`.
654
+ * @param {number} value
655
+ * @param {Message} [message]
656
+ * @returns {this}
657
+ */
658
+ min(value, message = `Must be greater than or equal to ${value}`) {
659
+ return this._rule((v) => v >= value, "too_small", message, { minimum: value });
660
+ }
661
+
662
+ /**
663
+ * At most `value`.
664
+ * @param {number} value
665
+ * @param {Message} [message]
666
+ * @returns {this}
667
+ */
668
+ max(value, message = `Must be less than or equal to ${value}`) {
669
+ return this._rule((v) => v <= value, "too_big", message, { maximum: value });
670
+ }
671
+
672
+ /**
673
+ * An integer.
674
+ * @param {Message} [message]
675
+ * @returns {this}
676
+ */
677
+ int(message = "Must be an integer") {
678
+ return this._rule(Number.isInteger, "invalid_type", message, { expected: "integer" });
679
+ }
680
+
681
+ /**
682
+ * Greater than 0.
683
+ * @param {Message} [message]
684
+ * @returns {this}
685
+ */
686
+ positive(message = "Must be positive") {
687
+ return this._rule((v) => v > 0, "too_small", message, { minimum: 0, exclusive: true });
688
+ }
689
+
690
+ /**
691
+ * 0 or more.
692
+ * @param {Message} [message]
693
+ * @returns {this}
694
+ */
695
+ nonnegative(message = "Must not be negative") {
696
+ return this._rule((v) => v >= 0, "too_small", message, { minimum: 0 });
697
+ }
698
+
699
+ /**
700
+ * Less than 0.
701
+ * @param {Message} [message]
702
+ * @returns {this}
703
+ */
704
+ negative(message = "Must be negative") {
705
+ return this._rule((v) => v < 0, "too_big", message, { maximum: 0, exclusive: true });
706
+ }
707
+
708
+ /**
709
+ * A multiple of `step`.
710
+ * @param {number} step
711
+ * @param {Message} [message]
712
+ * @returns {this}
713
+ */
714
+ 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 });
716
+ }
717
+ }
718
+
719
+ /**
720
+ * A boolean schema.
721
+ *
722
+ * @extends {Schema<boolean>}
723
+ */
724
+ class BooleanSchema extends Schema {
725
+ /** @protected */
726
+ _coerce = false;
727
+
728
+ /**
729
+ * @param {unknown} value
730
+ * @param {ParseContext} ctx
731
+ * @returns {unknown}
732
+ * @override
733
+ * @protected
734
+ */
735
+ _check(value, ctx) {
736
+ if (typeof value === "boolean") return value;
737
+ if (this._coerce) {
738
+ const text = String(value).trim().toLowerCase();
739
+ if (["true", "1", "yes", "on", "y"].includes(text)) return true;
740
+ if (["false", "0", "no", "off", "n", ""].includes(text)) return false;
741
+ }
742
+ return _typeIssue(ctx, "boolean", value, this._typeMessage);
743
+ }
744
+ }
745
+
746
+ /**
747
+ * A bigint schema.
748
+ *
749
+ * @extends {Schema<bigint>}
750
+ */
751
+ class BigIntSchema extends Schema {
752
+ /** @protected */
753
+ _coerce = false;
754
+
755
+ /**
756
+ * @param {unknown} value
757
+ * @param {ParseContext} ctx
758
+ * @returns {unknown}
759
+ * @override
760
+ * @protected
761
+ */
762
+ _check(value, ctx) {
763
+ if (typeof value === "bigint") return value;
764
+ if (this._coerce && (typeof value === "string" || typeof value === "number")) {
765
+ try {
766
+ return BigInt(value);
767
+ } catch {
768
+ // fall through to the error
769
+ }
770
+ }
771
+ return _typeIssue(ctx, "bigint", value, this._typeMessage);
772
+ }
773
+ }
774
+
775
+ /**
776
+ * A date schema.
777
+ *
778
+ * @extends {Schema<Date>}
779
+ */
780
+ class DateSchema extends Schema {
781
+ /** @protected */
782
+ _coerce = false;
783
+
784
+ /**
785
+ * @param {unknown} value
786
+ * @param {ParseContext} ctx
787
+ * @returns {unknown}
788
+ * @override
789
+ * @protected
790
+ */
791
+ _check(value, ctx) {
792
+ let v = value;
793
+ if (this._coerce && (typeof v === "string" || typeof v === "number")) v = new Date(v);
794
+ if (!types.isDate(v)) return _typeIssue(ctx, "date", value, this._typeMessage);
795
+ if (Number.isNaN(v.getTime())) return _issue(ctx, "invalid_date", "Invalid date");
796
+ return v;
797
+ }
798
+
799
+ /**
800
+ * On or after `date`.
801
+ * @param {Date | number | string} date
802
+ * @param {Message} [message]
803
+ * @returns {this}
804
+ */
805
+ min(date, message) {
806
+ const limit = new Date(date).getTime();
807
+ 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()}` });
808
+ }
809
+
810
+ /**
811
+ * On or before `date`.
812
+ * @param {Date | number | string} date
813
+ * @param {Message} [message]
814
+ * @returns {this}
815
+ */
816
+ max(date, message) {
817
+ const limit = new Date(date).getTime();
818
+ 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
+ }
820
+ }
821
+
822
+ /**
823
+ * A schema that accepts a fixed set of values.
824
+ *
825
+ * @template T
826
+ * @extends {Schema<T>}
827
+ */
828
+ class LiteralSchema extends Schema {
829
+ /** @param {readonly T[]} values */
830
+ constructor(values) {
831
+ super();
832
+ /** @protected */
833
+ this._values = values;
834
+ }
835
+
836
+ /**
837
+ * @param {unknown} value
838
+ * @param {ParseContext} ctx
839
+ * @returns {unknown}
840
+ * @override
841
+ * @protected
842
+ */
843
+ _check(value, ctx) {
844
+ if (this._values.some((v) => Object.is(v, value))) return value;
845
+ const list = this._values.map((v) => JSON.stringify(v)).join(" | ");
846
+ const code = this._values.length > 1 ? "invalid_enum_value" : "invalid_literal";
847
+ return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${JSON.stringify(value) ?? String(value)}`, { expected: list, received: _typeName(value) });
848
+ }
849
+
850
+ /** The accepted values. */
851
+ get options() {
852
+ return [...this._values];
853
+ }
854
+ }
855
+
856
+ /**
857
+ * A schema built from a test function.
858
+ *
859
+ * @template T
860
+ * @extends {Schema<T>}
861
+ */
862
+ class CustomSchema extends Schema {
863
+ /**
864
+ * @param {(value: unknown) => boolean} test
865
+ * @param {Message} message
866
+ * @param {string} expected
867
+ */
868
+ constructor(test, message, expected) {
869
+ super();
870
+ /** @protected */
871
+ this._test = test;
872
+ /** @protected */
873
+ this._message = message;
874
+ /** @protected */
875
+ this._expected = expected;
876
+ }
877
+
878
+ /**
879
+ * @param {unknown} value
880
+ * @param {ParseContext} ctx
881
+ * @returns {unknown}
882
+ * @override
883
+ * @protected
884
+ */
885
+ _check(value, ctx) {
886
+ if (this._test(value)) return value;
887
+ return _issue(ctx, this._expected === "custom" ? "custom" : "invalid_type", _message(this._message, value), { expected: this._expected, received: _typeName(value) });
888
+ }
889
+ }
890
+
891
+ /**
892
+ * An array schema.
893
+ *
894
+ * @template T
895
+ * @extends {Schema<T[]>}
896
+ */
897
+ class ArraySchema extends Schema {
898
+ /** @param {Schema<T>} item */
899
+ constructor(item) {
900
+ super();
901
+ /** @protected */
902
+ this._item = item;
903
+ }
904
+
905
+ /**
906
+ * @param {unknown} value
907
+ * @param {ParseContext} ctx
908
+ * @returns {unknown}
909
+ * @override
910
+ * @protected
911
+ */
912
+ _check(value, ctx) {
913
+ if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
914
+ const out = [];
915
+ for (let i = 0; i < value.length; i++) {
916
+ const saved = ctx.path;
917
+ ctx.path = [...saved, i];
918
+ out.push(_runChild(this._item, value[i], ctx));
919
+ ctx.path = saved;
920
+ }
921
+ return out;
922
+ }
923
+
924
+ /**
925
+ * @param {(v: T[]) => boolean} test
926
+ * @param {string} code
927
+ * @param {Message} message
928
+ * @returns {this}
929
+ * @protected
930
+ */
931
+ _rule(test, code, message) {
932
+ return this._with({ kind: "check", test, code, message });
933
+ }
934
+
935
+ /**
936
+ * At least `count` items.
937
+ * @param {number} count
938
+ * @param {Message} [message]
939
+ * @returns {this}
940
+ */
941
+ min(count, message = `Must contain at least ${count} item${count > 1 ? "s" : ""}`) {
942
+ return this._rule((v) => v.length >= count, "too_small", message);
943
+ }
944
+
945
+ /**
946
+ * At most `count` items.
947
+ * @param {number} count
948
+ * @param {Message} [message]
949
+ * @returns {this}
950
+ */
951
+ max(count, message = `Must contain at most ${count} item${count > 1 ? "s" : ""}`) {
952
+ return this._rule((v) => v.length <= count, "too_big", message);
953
+ }
954
+
955
+ /**
956
+ * Exactly `count` items.
957
+ * @param {number} count
958
+ * @param {Message} [message]
959
+ * @returns {this}
960
+ */
961
+ length(count, message = `Must contain exactly ${count} items`) {
962
+ return this._rule((v) => v.length === count, "invalid_length", message);
963
+ }
964
+
965
+ /**
966
+ * At least one item.
967
+ * @param {Message} [message]
968
+ * @returns {this}
969
+ */
970
+ nonempty(message = "Must not be empty") {
971
+ return this._rule((v) => v.length > 0, "too_small", message);
972
+ }
973
+
974
+ /**
975
+ * No duplicates, compared by value or by a key.
976
+ * @param {((item: T) => unknown) | Message} [keyOrMessage] A function returning the key, or a message.
977
+ * @param {Message} [message]
978
+ * @returns {this}
979
+ */
980
+ unique(keyOrMessage, message = "Must not contain duplicates") {
981
+ const key = typeof keyOrMessage === "function" && keyOrMessage.length === 1 ? /** @type {(item: T) => unknown} */ (keyOrMessage) : (/** @type {T} */ item) => item;
982
+ const msg = typeof keyOrMessage === "string" ? keyOrMessage : message;
983
+ return this._rule((v) => new Set(v.map(key)).size === v.length, "not_unique", msg);
984
+ }
985
+
986
+ /** The schema of the items. */
987
+ get element() {
988
+ return this._item;
989
+ }
990
+ }
991
+
992
+ /**
993
+ * An object schema.
994
+ *
995
+ * @template {Shape} S
996
+ * @extends {Schema<ObjectOutput<S>>}
997
+ */
998
+ class ObjectSchema extends Schema {
999
+ /** @protected @type {"strip" | "strict" | "passthrough"} */
1000
+ _unknown = "strip";
1001
+
1002
+ /** @param {S} shape */
1003
+ constructor(shape) {
1004
+ super();
1005
+ /** @protected */
1006
+ this._shape = shape;
1007
+ }
1008
+
1009
+ /**
1010
+ * @param {unknown} value
1011
+ * @param {ParseContext} ctx
1012
+ * @returns {unknown}
1013
+ * @override
1014
+ * @protected
1015
+ */
1016
+ _check(value, ctx) {
1017
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
1018
+ const input = /** @type {Record<string, unknown>} */ (value);
1019
+ /** @type {Record<string, unknown>} */
1020
+ const out = {};
1021
+ for (const [key, schema] of Object.entries(this._shape)) {
1022
+ const saved = ctx.path;
1023
+ ctx.path = [...saved, key];
1024
+ const result = _runChild(schema, input[key], ctx);
1025
+ ctx.path = saved;
1026
+ if (result !== undefined || Object.prototype.hasOwnProperty.call(input, key)) out[key] = result;
1027
+ }
1028
+ const unknownKeys = Object.keys(input).filter((key) => !(key in this._shape));
1029
+ if (unknownKeys.length) {
1030
+ if (this._unknown === "strict") _issue(ctx, "unrecognized_keys", `Unrecognized key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.join(", ")}`, { keys: unknownKeys });
1031
+ else if (this._unknown === "passthrough") {
1032
+ for (const key of unknownKeys) if (key !== "__proto__" && key !== "constructor" && key !== "prototype") out[key] = input[key];
1033
+ }
1034
+ }
1035
+ return out;
1036
+ }
1037
+
1038
+ /** The schema of each key. */
1039
+ get shape() {
1040
+ return this._shape;
1041
+ }
1042
+
1043
+ /**
1044
+ * Rejects unknown keys instead of dropping them.
1045
+ * @returns {this}
1046
+ */
1047
+ strict() {
1048
+ const copy = this._clone();
1049
+ copy._unknown = "strict";
1050
+ return copy;
1051
+ }
1052
+
1053
+ /**
1054
+ * Keeps unknown keys instead of dropping them.
1055
+ * @returns {this}
1056
+ */
1057
+ passthrough() {
1058
+ const copy = this._clone();
1059
+ copy._unknown = "passthrough";
1060
+ return copy;
1061
+ }
1062
+
1063
+ /**
1064
+ * Adds keys, or replaces existing ones.
1065
+ * @template {Shape} M
1066
+ * @param {M} more
1067
+ * @returns {ObjectSchema<Omit<S, keyof M> & M>}
1068
+ */
1069
+ extend(more) {
1070
+ const copy = /** @type {any} */ (object({ ...this._shape, ...more }));
1071
+ copy._unknown = this._unknown;
1072
+ return copy;
1073
+ }
1074
+
1075
+ /**
1076
+ * Keeps only some keys.
1077
+ * @template {keyof S & string} K
1078
+ * @param {readonly K[]} keys
1079
+ * @returns {ObjectSchema<Pick<S, K>>}
1080
+ */
1081
+ pick(keys) {
1082
+ /** @type {any} */
1083
+ const shape = {};
1084
+ for (const key of keys) shape[key] = this._shape[key];
1085
+ return object(shape);
1086
+ }
1087
+
1088
+ /**
1089
+ * Drops some keys.
1090
+ * @template {keyof S & string} K
1091
+ * @param {readonly K[]} keys
1092
+ * @returns {ObjectSchema<Omit<S, K>>}
1093
+ */
1094
+ omit(keys) {
1095
+ /** @type {any} */
1096
+ const shape = { ...this._shape };
1097
+ for (const key of keys) delete shape[key];
1098
+ return object(shape);
1099
+ }
1100
+
1101
+ /**
1102
+ * Makes every key optional, which suits PATCH requests.
1103
+ * @returns {ObjectSchema<{ [K in keyof S]: Schema<Infer<S[K]> | undefined> }>}
1104
+ */
1105
+ partial() {
1106
+ /** @type {any} */
1107
+ const shape = {};
1108
+ for (const [key, schema] of Object.entries(this._shape)) shape[key] = schema.optional();
1109
+ return object(shape);
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * A schema that accepts any of several schemas.
1115
+ *
1116
+ * @template T
1117
+ * @extends {Schema<T>}
1118
+ */
1119
+ class UnionSchema extends Schema {
1120
+ /** @param {ReadonlyArray<Schema<any>>} options */
1121
+ constructor(options) {
1122
+ super();
1123
+ /** @protected */
1124
+ this._options = options;
1125
+ }
1126
+
1127
+ /**
1128
+ * @param {unknown} value
1129
+ * @param {ParseContext} ctx
1130
+ * @returns {unknown}
1131
+ * @override
1132
+ * @protected
1133
+ */
1134
+ _check(value, ctx) {
1135
+ /** @type {import("./errors").ValidationIssue[][]} */
1136
+ const failures = [];
1137
+ for (const option of this._options) {
1138
+ const attempt = { issues: [], path: ctx.path };
1139
+ const out = _runChild(option, value, attempt);
1140
+ if (!attempt.issues.length) return out;
1141
+ failures.push(attempt.issues);
1142
+ }
1143
+ const typeOnly = failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === ctx.path.length);
1144
+ if (typeOnly) {
1145
+ const expected = failures.map((issues) => issues[0].expected).join(" | ");
1146
+ return _issue(ctx, "invalid_union", `Expected ${expected}, received ${_typeName(value)}`, { expected, received: _typeName(value) });
1147
+ }
1148
+ const best = failures.reduce((a, b) => (b.length < a.length ? b : a));
1149
+ ctx.issues.push(...best);
1150
+ return INVALID;
1151
+ }
1152
+ }
1153
+
1154
+ /**
1155
+ * A fixed-length array schema.
1156
+ *
1157
+ * @template {any[]} T
1158
+ * @extends {Schema<T>}
1159
+ */
1160
+ class TupleSchema extends Schema {
1161
+ /** @param {ReadonlyArray<Schema<any>>} items */
1162
+ constructor(items) {
1163
+ super();
1164
+ /** @protected */
1165
+ this._items = items;
1166
+ }
1167
+
1168
+ /**
1169
+ * @param {unknown} value
1170
+ * @param {ParseContext} ctx
1171
+ * @returns {unknown}
1172
+ * @override
1173
+ * @protected
1174
+ */
1175
+ _check(value, ctx) {
1176
+ if (!Array.isArray(value)) return _typeIssue(ctx, "tuple", value, this._typeMessage);
1177
+ if (value.length !== this._items.length) {
1178
+ return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
1179
+ }
1180
+ return this._items.map((schema, i) => {
1181
+ const saved = ctx.path;
1182
+ ctx.path = [...saved, i];
1183
+ const out = _runChild(schema, value[i], ctx);
1184
+ ctx.path = saved;
1185
+ return out;
1186
+ });
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * A dictionary schema.
1192
+ *
1193
+ * @template V
1194
+ * @extends {Schema<Record<string, V>>}
1195
+ */
1196
+ class RecordSchema extends Schema {
1197
+ /**
1198
+ * @param {Schema<V>} values
1199
+ * @param {Schema<string> | undefined} keys
1200
+ */
1201
+ constructor(values, keys) {
1202
+ super();
1203
+ /** @protected */
1204
+ this._values = values;
1205
+ /** @protected */
1206
+ this._keys = keys;
1207
+ }
1208
+
1209
+ /**
1210
+ * @param {unknown} value
1211
+ * @param {ParseContext} ctx
1212
+ * @returns {unknown}
1213
+ * @override
1214
+ * @protected
1215
+ */
1216
+ _check(value, ctx) {
1217
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
1218
+ /** @type {Record<string, unknown>} */
1219
+ const out = {};
1220
+ for (const [key, item] of Object.entries(value)) {
1221
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
1222
+ const saved = ctx.path;
1223
+ ctx.path = [...saved, key];
1224
+ const k = this._keys ? _runChild(this._keys, key, ctx) : key;
1225
+ const v = _runChild(this._values, item, ctx);
1226
+ ctx.path = saved;
1227
+ if (typeof k === "string") out[k] = v;
1228
+ }
1229
+ return out;
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * A schema resolved on first use, for recursive data.
1235
+ *
1236
+ * @template T
1237
+ * @extends {Schema<T>}
1238
+ */
1239
+ class LazySchema extends Schema {
1240
+ /** @param {() => Schema<T>} getter */
1241
+ constructor(getter) {
1242
+ super();
1243
+ /** @protected */
1244
+ this._getter = getter;
1245
+ }
1246
+
1247
+ /**
1248
+ * @param {unknown} value
1249
+ * @param {ParseContext} ctx
1250
+ * @returns {unknown}
1251
+ * @override
1252
+ * @protected
1253
+ */
1254
+ _check(value, ctx) {
1255
+ const before = ctx.issues.length;
1256
+ const out = _runChild(this._getter(), value, ctx);
1257
+ return ctx.issues.length > before ? INVALID : out;
1258
+ }
1259
+ }
1260
+
1261
+ /**
1262
+ * A string schema.
1263
+ *
1264
+ * @example
1265
+ * s.string().trim().min(1).max(100);
1266
+ * s.string().email();
1267
+ *
1268
+ * @param {{ message?: Message }} [options] Message when the value isn't a string.
1269
+ * @returns {StringSchema}
1270
+ */
1271
+ function string(options = {}) {
1272
+ const schema = new StringSchema();
1273
+ /** @type {any} */ (schema)._typeMessage = options.message;
1274
+ return schema;
1275
+ }
1276
+
1277
+ /**
1278
+ * A number. `NaN` and `Infinity` are refused.
1279
+ *
1280
+ * @example
1281
+ * s.number().int().min(0).max(120);
1282
+ *
1283
+ * @param {{ message?: Message }} [options] Message when the value isn't a number.
1284
+ * @returns {NumberSchema}
1285
+ */
1286
+ function number(options = {}) {
1287
+ const schema = new NumberSchema();
1288
+ /** @type {any} */ (schema)._typeMessage = options.message;
1289
+ return schema;
1290
+ }
1291
+
1292
+ /**
1293
+ * A boolean schema.
1294
+ *
1295
+ * @param {{ message?: Message }} [options]
1296
+ * @returns {BooleanSchema}
1297
+ */
1298
+ function boolean(options = {}) {
1299
+ const schema = new BooleanSchema();
1300
+ /** @type {any} */ (schema)._typeMessage = options.message;
1301
+ return schema;
1302
+ }
1303
+
1304
+ /**
1305
+ * A bigint schema.
1306
+ *
1307
+ * @param {{ message?: Message }} [options]
1308
+ * @returns {BigIntSchema}
1309
+ */
1310
+ function bigint(options = {}) {
1311
+ const schema = new BigIntSchema();
1312
+ /** @type {any} */ (schema)._typeMessage = options.message;
1313
+ return schema;
1314
+ }
1315
+
1316
+ /**
1317
+ * A valid `Date`.
1318
+ *
1319
+ * @example
1320
+ * s.date().min("2024-01-01");
1321
+ *
1322
+ * @param {{ message?: Message }} [options]
1323
+ * @returns {DateSchema}
1324
+ */
1325
+ function date(options = {}) {
1326
+ const schema = new DateSchema();
1327
+ /** @type {any} */ (schema)._typeMessage = options.message;
1328
+ return schema;
1329
+ }
1330
+
1331
+ /**
1332
+ * Exactly this value.
1333
+ *
1334
+ * @example
1335
+ * s.object({ type: s.literal("circle"), radius: s.number() });
1336
+ *
1337
+ * @template {string | number | boolean | bigint | null | undefined} L
1338
+ * @param {L} value
1339
+ * @returns {LiteralSchema<L>}
1340
+ */
1341
+ function literal(value) {
1342
+ return new LiteralSchema([value]);
1343
+ }
1344
+
1345
+ /**
1346
+ * One of these values. The type is their union.
1347
+ *
1348
+ * @example
1349
+ * const Role = s.enum(["admin", "editor", "viewer"]); // Schema<"admin" | "editor" | "viewer">
1350
+ * Role.options; // ["admin", "editor", "viewer"]
1351
+ *
1352
+ * @template {string | number} V
1353
+ * @param {readonly V[]} values
1354
+ * @param {{ message?: Message }} [options]
1355
+ * @returns {LiteralSchema<V>}
1356
+ */
1357
+ function enumeration(values, options = {}) {
1358
+ const schema = new LiteralSchema(values);
1359
+ /** @type {any} */ (schema)._typeMessage = options.message;
1360
+ return schema;
1361
+ }
1362
+
1363
+ /**
1364
+ * An array whose items all match `item`.
1365
+ *
1366
+ * @example
1367
+ * s.array(s.string()).min(1).max(10).unique();
1368
+ *
1369
+ * @template T
1370
+ * @param {Schema<T>} item
1371
+ * @returns {ArraySchema<T>}
1372
+ */
1373
+ function array(item) {
1374
+ return new ArraySchema(item);
1375
+ }
1376
+
1377
+ /**
1378
+ * An object with known keys. Unknown keys are dropped; `.strict()` rejects
1379
+ * them and `.passthrough()` keeps them.
1380
+ *
1381
+ * @example
1382
+ * const Address = s.object({ street: s.string(), zip: s.string().regex(/^\d{5}$/), city: s.string() });
1383
+ *
1384
+ * @template {Shape} S
1385
+ * @param {S} shape
1386
+ * @returns {ObjectSchema<S>}
1387
+ */
1388
+ function object(shape) {
1389
+ return new ObjectSchema(shape);
1390
+ }
1391
+
1392
+ /**
1393
+ * Matches any of these schemas; the first that fits wins.
1394
+ *
1395
+ * @example
1396
+ * s.union([s.string(), s.number()]); // Schema<string | number>
1397
+ *
1398
+ * @template {ReadonlyArray<Schema<any>>} U
1399
+ * @param {U} options
1400
+ * @returns {UnionSchema<Infer<U[number]>>}
1401
+ */
1402
+ function union(options) {
1403
+ return new UnionSchema(options);
1404
+ }
1405
+
1406
+ /**
1407
+ * A fixed-length array with one schema per position.
1408
+ *
1409
+ * @example
1410
+ * s.tuple([s.number(), s.number()]); // Schema<[number, number]>
1411
+ *
1412
+ * @template {[Schema<any>, ...Schema<any>[]] | []} T
1413
+ * @param {T} items
1414
+ * @returns {TupleSchema<{ -readonly [K in keyof T]: Infer<T[K]> }>}
1415
+ */
1416
+ function tuple(items) {
1417
+ return /** @type {any} */ (new TupleSchema(items));
1418
+ }
1419
+
1420
+ /**
1421
+ * An object with any keys whose values match a schema, like a dictionary.
1422
+ *
1423
+ * @example
1424
+ * s.record(s.number()); // Record<string, number>
1425
+ * s.record(s.boolean(), s.string().regex(/^[a-z]+$/)); // keys checked too
1426
+ *
1427
+ * @template V
1428
+ * @param {Schema<V>} values
1429
+ * @param {Schema<string>} [keys]
1430
+ * @returns {RecordSchema<V>}
1431
+ */
1432
+ function record(values, keys) {
1433
+ return new RecordSchema(values, keys);
1434
+ }
1435
+
1436
+ /**
1437
+ * A schema defined later, for recursive data like trees.
1438
+ *
1439
+ * @example
1440
+ * const Category = s.object({
1441
+ * name: s.string(),
1442
+ * children: s.lazy(() => s.array(Category)).default([]),
1443
+ * });
1444
+ *
1445
+ * @template T
1446
+ * @param {() => Schema<T>} getter
1447
+ * @returns {LazySchema<T>}
1448
+ */
1449
+ function lazy(getter) {
1450
+ return new LazySchema(getter);
1451
+ }
1452
+
1453
+ /**
1454
+ * An instance of a class.
1455
+ *
1456
+ * @example
1457
+ * s.instanceOf(URL);
1458
+ *
1459
+ * @template {new (...args: any[]) => any} C
1460
+ * @param {C} ctor
1461
+ * @param {Message} [message]
1462
+ * @returns {CustomSchema<InstanceType<C>>}
1463
+ */
1464
+ function instanceOf(ctor, message = `Expected an instance of ${ctor.name}`) {
1465
+ return new CustomSchema((value) => value instanceof ctor, message, ctor.name);
1466
+ }
1467
+
1468
+ /**
1469
+ * A schema from any test, usually a type guard.
1470
+ *
1471
+ * @example
1472
+ * const Color = s.custom(nc.isHexColor, "Expected a hex color");
1473
+ *
1474
+ * @template [T=unknown]
1475
+ * @param {((value: unknown) => value is T) | ((value: unknown) => boolean)} test
1476
+ * @param {Message} [message]
1477
+ * @returns {CustomSchema<T>}
1478
+ */
1479
+ function custom(test, message = "Invalid value") {
1480
+ return new CustomSchema(test, message, "custom");
1481
+ }
1482
+
1483
+ /**
1484
+ * Anything, typed as `any`.
1485
+ * @returns {Schema<any>}
1486
+ */
1487
+ function any() {
1488
+ return new Schema();
1489
+ }
1490
+
1491
+ /**
1492
+ * Anything, typed as `unknown`.
1493
+ * @returns {Schema<unknown>}
1494
+ */
1495
+ function unknown() {
1496
+ return new Schema();
1497
+ }
1498
+
1499
+ /**
1500
+ * The `s.coerce` builders.
1501
+ * @typedef {object} CoerceBuilders
1502
+ * @property {() => StringSchema} string A string schema that turns any value into a string first.
1503
+ * @property {() => NumberSchema} number A number schema that converts strings like `"42"`, booleans and dates first.
1504
+ * @property {() => BooleanSchema} boolean A boolean schema that understands `"true"`, `"1"`, `"yes"`, `"on"` and their opposites.
1505
+ * @property {() => DateSchema} date A date schema that converts ISO strings and timestamps.
1506
+ * @property {() => BigIntSchema} bigint A bigint schema that converts numeric strings and integers.
1507
+ */
1508
+
1509
+ /**
1510
+ * Schemas that convert their input before checking it. Query strings, form
1511
+ * fields and environment variables are always strings, so this is what you
1512
+ * want for them.
1513
+ *
1514
+ * @example
1515
+ * const Query = s.object({
1516
+ * page: s.coerce.number().int().min(1).default(1),
1517
+ * active: s.coerce.boolean().default(false),
1518
+ * });
1519
+ * Query.parse({ page: "2", active: "true" }); // { page: 2, active: true }
1520
+ *
1521
+ * @type {CoerceBuilders}
1522
+ */
1523
+ const coerce = {
1524
+ /**
1525
+ * @returns {StringSchema}
1526
+ */
1527
+ string() {
1528
+ const schema = new StringSchema();
1529
+ /** @type {any} */ (schema)._coerce = true;
1530
+ return schema;
1531
+ },
1532
+ /**
1533
+ * @returns {NumberSchema}
1534
+ */
1535
+ number() {
1536
+ const schema = new NumberSchema();
1537
+ /** @type {any} */ (schema)._coerce = true;
1538
+ return schema;
1539
+ },
1540
+ /**
1541
+ * @returns {BooleanSchema}
1542
+ */
1543
+ boolean() {
1544
+ const schema = new BooleanSchema();
1545
+ /** @type {any} */ (schema)._coerce = true;
1546
+ return schema;
1547
+ },
1548
+ /**
1549
+ * @returns {DateSchema}
1550
+ */
1551
+ date() {
1552
+ const schema = new DateSchema();
1553
+ /** @type {any} */ (schema)._coerce = true;
1554
+ return schema;
1555
+ },
1556
+ /**
1557
+ * @returns {BigIntSchema}
1558
+ */
1559
+ bigint() {
1560
+ const schema = new BigIntSchema();
1561
+ /** @type {any} */ (schema)._coerce = true;
1562
+ return schema;
1563
+ },
1564
+ };
1565
+
1566
+ module.exports = {
1567
+ string,
1568
+ number,
1569
+ boolean,
1570
+ bigint,
1571
+ date,
1572
+ literal,
1573
+ enum: enumeration,
1574
+ array,
1575
+ object,
1576
+ union,
1577
+ tuple,
1578
+ record,
1579
+ lazy,
1580
+ instanceOf,
1581
+ custom,
1582
+ any,
1583
+ unknown,
1584
+ coerce,
1585
+ Schema,
1586
+ };