@ix-xs/node-comfort 1.1.31 → 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.
- package/CHANGELOG.md +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- 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
|
+
};
|