@ix-xs/node-comfort 1.2.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Schema.js
ADDED
|
@@ -0,0 +1,2101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate data with schemas, in the spirit of `zod`. Describe your data
|
|
5
|
+
* once and get both a runtime check with clear messages and the TypeScript
|
|
6
|
+
* type, `Infer<typeof schema>`. Schemas never change: `.min()` or
|
|
7
|
+
* `.optional()` return a new one.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const User = s.object({
|
|
11
|
+
* name: s.string().trim().min(2),
|
|
12
|
+
* email: s.string().email().toLowerCase(),
|
|
13
|
+
* age: s.number().int().min(18).optional(),
|
|
14
|
+
* role: s.enum(["admin", "user"]).default("user"),
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const user = User.parse(req.body); // typed, or a ValidationError listing every issue
|
|
18
|
+
* const result = User.safeParse(input); // { success, data } or { success, error, issues }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { types } = require("node:util");
|
|
22
|
+
const { ValidationError } = require("./errors.js");
|
|
23
|
+
const { UNSAFE_KEYS } = require("./internal/shared.js");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The type a schema produces.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // type User = Infer<typeof UserSchema>;
|
|
30
|
+
*
|
|
31
|
+
* @template S
|
|
32
|
+
* @typedef {S extends Schema<infer T> ? T : never} Infer
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An object of schemas, as passed to `object()`.
|
|
37
|
+
* @typedef {Record<string, Schema<any>>} Shape
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Flattens a type so editors show it nicely.
|
|
42
|
+
* @template T
|
|
43
|
+
* @typedef {{ [K in keyof T]: T[K] } & {}} Simplify
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The type of an object schema. Keys that accept `undefined` become optional.
|
|
48
|
+
* @template {Shape} S
|
|
49
|
+
* @typedef {Simplify<{ [K in keyof S as undefined extends Infer<S[K]> ? never : K]: Infer<S[K]> } & { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]> }>} ObjectOutput
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What `safeParse()` returns. On failure, `issues` is the plain list and
|
|
54
|
+
* `error` the same thing as a `ValidationError`, built when you read it.
|
|
55
|
+
* @template T
|
|
56
|
+
* @typedef {{ success: true, data: T, error?: undefined, issues?: undefined } | { success: false, data?: undefined, error: ValidationError, issues: import("./errors").ValidationIssue[] }} SafeParseResult
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An error message: a string, or a function that gets the invalid value.
|
|
61
|
+
* @typedef {string | ((value: any) => string)} Message
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Options for `refine()`.
|
|
66
|
+
* @typedef {object} RefineOptions
|
|
67
|
+
* @property {Message} [message] Defaults to `"Invalid value"`.
|
|
68
|
+
* @property {Array<string | number>} [path] Where to report the issue, like `["confirmPassword"]` for a cross-field check.
|
|
69
|
+
* @property {string} [code] Defaults to `"custom"`.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {{ issues: import("./errors").ValidationIssue[], path: Array<string | number> }} ParseContext
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A step in a schema: a check, a transformation, a refinement or a pipe
|
|
78
|
+
* into another schema.
|
|
79
|
+
* @typedef {{ kind: "check", test: (value: any) => boolean, code: string, message: Message, details?: Record<string, unknown>, json?: Record<string, unknown> } | { kind: "transform", fn: (value: any) => any } | { kind: "refine", test: (value: any) => boolean, options: RefineOptions } | { kind: "pipe", schema: Schema<any> }} Step
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A JSON Schema, as made by `toJSONSchema()`.
|
|
84
|
+
* @typedef {{ [keyword: string]: any }} JSONSchema
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {{ defs: Record<string, JSONSchema>, names: Map<Schema<any>, string> }} _JSONContext
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
const INVALID = Symbol("invalid");
|
|
92
|
+
|
|
93
|
+
const _TRUE = ["true", "1", "yes", "on", "y"];
|
|
94
|
+
const _FALSE = ["false", "0", "no", "off", "n", ""];
|
|
95
|
+
const _DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* What a failed `safeParse()` returns. The error is only built if it's
|
|
99
|
+
* read: capturing a stack trace costs far more than the validation itself.
|
|
100
|
+
* @param {import("./errors").ValidationIssue[]} issues
|
|
101
|
+
* @returns {{ success: false, error: ValidationError, issues: import("./errors").ValidationIssue[] }}
|
|
102
|
+
*/
|
|
103
|
+
const _failure = (issues) => {
|
|
104
|
+
/** @type {ValidationError | undefined} */
|
|
105
|
+
let error;
|
|
106
|
+
return /** @type {any} */ (Object.defineProperty({ success: false, issues }, "error", { enumerable: true, get: () => (error ??= new ValidationError(issues)) }));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {Schema<any>} schema
|
|
111
|
+
* @param {_JSONContext} ctx
|
|
112
|
+
* @returns {JSONSchema}
|
|
113
|
+
*/
|
|
114
|
+
const _jsonOf = (schema, ctx) => /** @type {any} */ (schema)._toJSON(ctx);
|
|
115
|
+
|
|
116
|
+
/** @param {unknown} value */
|
|
117
|
+
const _typeName = (value) => {
|
|
118
|
+
if (value === null) return "null";
|
|
119
|
+
if (Array.isArray(value)) return "array";
|
|
120
|
+
if (types.isDate(value)) return Number.isNaN(value.getTime()) ? "invalid date" : "date";
|
|
121
|
+
if (typeof value === "number" && Number.isNaN(value)) return "NaN";
|
|
122
|
+
return typeof value;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Message} message
|
|
127
|
+
* @param {unknown} value
|
|
128
|
+
*/
|
|
129
|
+
const _message = (message, value) => (typeof message === "function" ? message(value) : message);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {ParseContext} ctx
|
|
133
|
+
* @param {string} code
|
|
134
|
+
* @param {string} message
|
|
135
|
+
* @param {Record<string, unknown>} [details]
|
|
136
|
+
*/
|
|
137
|
+
const _issue = (ctx, code, message, details) => {
|
|
138
|
+
ctx.issues.push({ path: [...ctx.path], code, message, ...details });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {ParseContext} ctx
|
|
143
|
+
* @param {string} expected
|
|
144
|
+
* @param {unknown} value
|
|
145
|
+
* @param {Message} [message]
|
|
146
|
+
*/
|
|
147
|
+
const _typeIssue = (ctx, expected, value, message) => {
|
|
148
|
+
const received = _typeName(value);
|
|
149
|
+
/** @protected */
|
|
150
|
+
_issue(ctx, "invalid_type", message ? _message(message, value) : value === undefined ? "Required" : `Expected ${expected}, received ${received}`, { expected, received });
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Runs a nested schema.
|
|
155
|
+
* @param {Schema<any>} schema
|
|
156
|
+
* @param {unknown} value
|
|
157
|
+
* @param {ParseContext} ctx
|
|
158
|
+
* @returns {unknown}
|
|
159
|
+
*/
|
|
160
|
+
const _runChild = (schema, value, ctx) => /** @type {any} */ (schema)._run(value, ctx);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The base of every schema.
|
|
164
|
+
*
|
|
165
|
+
* @template [T=unknown]
|
|
166
|
+
*/
|
|
167
|
+
class Schema {
|
|
168
|
+
/** @protected @type {Step[]} */
|
|
169
|
+
_steps = [];
|
|
170
|
+
/** @protected */
|
|
171
|
+
_optional = false;
|
|
172
|
+
/** @protected */
|
|
173
|
+
_nullable = false;
|
|
174
|
+
/** @protected @type {{ value: unknown } | undefined} */
|
|
175
|
+
_default = undefined;
|
|
176
|
+
/** @protected @type {{ value: unknown } | undefined} */
|
|
177
|
+
_catch = undefined;
|
|
178
|
+
/** @protected @type {Message | undefined} */
|
|
179
|
+
_typeMessage = undefined;
|
|
180
|
+
/** @protected @type {string | undefined} */
|
|
181
|
+
_description = undefined;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Checks the type, and coerces when asked. Subclasses override it.
|
|
185
|
+
* @param {unknown} value
|
|
186
|
+
* @param {ParseContext} _ctx
|
|
187
|
+
* @returns {unknown}
|
|
188
|
+
* @protected
|
|
189
|
+
*/
|
|
190
|
+
_check(value, _ctx) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @returns {this}
|
|
196
|
+
* @protected
|
|
197
|
+
*/
|
|
198
|
+
_clone() {
|
|
199
|
+
const copy = Object.create(Object.getPrototypeOf(this));
|
|
200
|
+
Object.assign(copy, this);
|
|
201
|
+
copy._steps = [...this._steps];
|
|
202
|
+
return copy;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {Step} step
|
|
207
|
+
* @returns {this}
|
|
208
|
+
* @protected
|
|
209
|
+
*/
|
|
210
|
+
_with(step) {
|
|
211
|
+
const copy = this._clone();
|
|
212
|
+
copy._steps.push(step);
|
|
213
|
+
return copy;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {unknown} input
|
|
218
|
+
* @param {ParseContext} ctx
|
|
219
|
+
* @returns {unknown} The output, or `INVALID`.
|
|
220
|
+
* @protected
|
|
221
|
+
*/
|
|
222
|
+
_run(input, ctx) {
|
|
223
|
+
let value = input;
|
|
224
|
+
if (value === undefined && this._default) {
|
|
225
|
+
const def = this._default.value;
|
|
226
|
+
value = typeof def === "function" ? def() : def !== null && typeof def === "object" ? require("./Obj.js").clone(def) : def;
|
|
227
|
+
}
|
|
228
|
+
if (value === undefined && this._optional) return undefined;
|
|
229
|
+
if (value === null && this._nullable) return null;
|
|
230
|
+
const before = ctx.issues.length;
|
|
231
|
+
let out = this._check(value, ctx);
|
|
232
|
+
if (ctx.issues.length === before) {
|
|
233
|
+
for (const step of this._steps) {
|
|
234
|
+
if (step.kind === "check") {
|
|
235
|
+
if (!step.test(out)) _issue(ctx, step.code, _message(step.message, out), step.details);
|
|
236
|
+
} else if (ctx.issues.length > before) {
|
|
237
|
+
// What follows needs a value that passed everything before it.
|
|
238
|
+
if (step.kind !== "refine") break;
|
|
239
|
+
} else if (step.kind === "transform") {
|
|
240
|
+
out = step.fn(out);
|
|
241
|
+
} else if (step.kind === "pipe") {
|
|
242
|
+
out = _runChild(step.schema, out, ctx);
|
|
243
|
+
} else if (!step.test(out)) {
|
|
244
|
+
const saved = ctx.path;
|
|
245
|
+
if (step.options.path) ctx.path = [...saved, ...step.options.path];
|
|
246
|
+
_issue(ctx, step.options.code ?? "custom", _message(step.options.message ?? "Invalid value", out));
|
|
247
|
+
ctx.path = saved;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (ctx.issues.length > before) {
|
|
252
|
+
if (this._catch) {
|
|
253
|
+
ctx.issues.length = before;
|
|
254
|
+
const fallback = this._catch.value;
|
|
255
|
+
return typeof fallback === "function" ? fallback(input) : fallback;
|
|
256
|
+
}
|
|
257
|
+
return INVALID;
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Validates a value and returns it, with defaults and transformations
|
|
264
|
+
* applied. Unknown object keys are dropped.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* const port = s.coerce.number().int().min(1).max(65535).parse(process.env.PORT);
|
|
268
|
+
*
|
|
269
|
+
* @param {unknown} value
|
|
270
|
+
* @returns {T}
|
|
271
|
+
* @throws {ValidationError} Listing every issue with its path.
|
|
272
|
+
*/
|
|
273
|
+
parse(value) {
|
|
274
|
+
const ctx = { issues: [], path: [] };
|
|
275
|
+
const out = this._run(value, ctx);
|
|
276
|
+
if (ctx.issues.length) throw new ValidationError(ctx.issues);
|
|
277
|
+
return /** @type {T} */ (out);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validates a value without throwing.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* const result = User.safeParse(req.body);
|
|
285
|
+
* if (!result.success) return res.status(400).json(result.error.flatten());
|
|
286
|
+
* saveUser(result.data);
|
|
287
|
+
*
|
|
288
|
+
* @param {unknown} value
|
|
289
|
+
* @returns {SafeParseResult<T>}
|
|
290
|
+
*/
|
|
291
|
+
safeParse(value) {
|
|
292
|
+
/** @type {ParseContext} */
|
|
293
|
+
const ctx = { issues: [], path: [] };
|
|
294
|
+
const out = this._run(value, ctx);
|
|
295
|
+
if (ctx.issues.length) return _failure(ctx.issues);
|
|
296
|
+
return { success: true, data: /** @type {T} */ (out) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Is the value valid? Works as a type guard, and builds no error.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* if (Email.is(input)) sendMail(input); // input is a string here
|
|
304
|
+
*
|
|
305
|
+
* @param {unknown} value
|
|
306
|
+
* @returns {value is T}
|
|
307
|
+
*/
|
|
308
|
+
is(value) {
|
|
309
|
+
/** @type {ParseContext} */
|
|
310
|
+
const ctx = { issues: [], path: [] };
|
|
311
|
+
this._run(value, ctx);
|
|
312
|
+
return ctx.issues.length === 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Also accepts `undefined`. In an object, the key becomes optional.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* s.object({ nickname: s.string().optional() }); // { nickname?: string }
|
|
320
|
+
*
|
|
321
|
+
* @returns {Schema<T | undefined>}
|
|
322
|
+
*/
|
|
323
|
+
optional() {
|
|
324
|
+
const copy = this._clone();
|
|
325
|
+
copy._optional = true;
|
|
326
|
+
return copy;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Also accepts `null`.
|
|
331
|
+
*
|
|
332
|
+
* @returns {Schema<T | null>}
|
|
333
|
+
*/
|
|
334
|
+
nullable() {
|
|
335
|
+
const copy = this._clone();
|
|
336
|
+
copy._nullable = true;
|
|
337
|
+
return copy;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Also accepts `null` and `undefined`.
|
|
342
|
+
*
|
|
343
|
+
* @returns {Schema<T | null | undefined>}
|
|
344
|
+
*/
|
|
345
|
+
nullish() {
|
|
346
|
+
const copy = this._clone();
|
|
347
|
+
copy._nullable = true;
|
|
348
|
+
copy._optional = true;
|
|
349
|
+
return copy;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Uses this value when the input is `undefined`. Objects are copied each
|
|
354
|
+
* time; pass a function to build a fresh value on every parse.
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* s.string().default("guest");
|
|
358
|
+
* s.date().default(() => new Date());
|
|
359
|
+
*
|
|
360
|
+
* @param {Exclude<T, undefined> | (() => Exclude<T, undefined>)} value
|
|
361
|
+
* @returns {Schema<Exclude<T, undefined>>}
|
|
362
|
+
*/
|
|
363
|
+
default(value) {
|
|
364
|
+
const copy = this._clone();
|
|
365
|
+
copy._default = { value };
|
|
366
|
+
return /** @type {any} */ (copy);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Returns this value instead of failing when the input is invalid.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* s.number().catch(0).parse("not a number"); // 0
|
|
374
|
+
*
|
|
375
|
+
* @param {T | ((input: unknown) => T)} value A value, or a function that gets the invalid input.
|
|
376
|
+
* @returns {Schema<T>}
|
|
377
|
+
*/
|
|
378
|
+
catch(value) {
|
|
379
|
+
const copy = this._clone();
|
|
380
|
+
copy._catch = { value };
|
|
381
|
+
return /** @type {any} */ (copy);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Changes the value once it's valid. Runs after the checks written before it.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* s.string().transform((value) => value.split(",")); // Schema<string[]>
|
|
389
|
+
*
|
|
390
|
+
* @template U
|
|
391
|
+
* @param {(value: T) => U} fn
|
|
392
|
+
* @returns {Schema<U>}
|
|
393
|
+
*/
|
|
394
|
+
transform(fn) {
|
|
395
|
+
return /** @type {any} */ (this._with({ kind: "transform", fn }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Adds your own rule.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* s.object({ password: s.string(), confirm: s.string() })
|
|
403
|
+
* .refine((v) => v.password === v.confirm, { message: "Passwords don't match", path: ["confirm"] });
|
|
404
|
+
*
|
|
405
|
+
* @param {(value: T) => boolean} test Returns `true` when the value is fine.
|
|
406
|
+
* @param {Message | RefineOptions} [options]
|
|
407
|
+
* @returns {this}
|
|
408
|
+
*/
|
|
409
|
+
refine(test, options = {}) {
|
|
410
|
+
const opts = typeof options === "string" || typeof options === "function" ? { message: options } : options;
|
|
411
|
+
return this._with({ kind: "refine", test, options: opts });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Accepts values that match this schema or another one.
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* s.string().or(s.number()); // Schema<string | number>
|
|
419
|
+
*
|
|
420
|
+
* @template U
|
|
421
|
+
* @param {Schema<U>} other
|
|
422
|
+
* @returns {Schema<T | U>}
|
|
423
|
+
*/
|
|
424
|
+
or(other) {
|
|
425
|
+
return union([/** @type {Schema<T>} */ (this), other]);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* An array of this schema.
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* s.string().email().array(); // Schema<string[]>
|
|
433
|
+
*
|
|
434
|
+
* @returns {ArraySchema<T>}
|
|
435
|
+
*/
|
|
436
|
+
array() {
|
|
437
|
+
return array(/** @type {Schema<T>} */ (this));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Sends the result into another schema. With `transform()` in between,
|
|
442
|
+
* it validates what a conversion produced.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* const Port = s.string().transform(Number).pipe(s.number().int().min(1).max(65535));
|
|
446
|
+
* Port.parse("8080"); // 8080
|
|
447
|
+
* Port.parse("abc"); // ValidationError: Expected number, received NaN
|
|
448
|
+
*
|
|
449
|
+
* @template U
|
|
450
|
+
* @param {Schema<U>} next
|
|
451
|
+
* @returns {Schema<U>}
|
|
452
|
+
*/
|
|
453
|
+
pipe(next) {
|
|
454
|
+
return /** @type {any} */ (this._with({ kind: "pipe", schema: next }));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Describes what this schema accepts as a JSON Schema (draft 2020-12).
|
|
459
|
+
* Use it for an OpenAPI document, a form generator, or the input of a
|
|
460
|
+
* tool you give to a language model. Rules that only exist in code,
|
|
461
|
+
* like `refine()`, `transform()` and what follows a `pipe()`, can't be
|
|
462
|
+
* expressed and are left out.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* s.object({ name: s.string().min(2), age: s.number().int().optional() }).toJSONSchema();
|
|
466
|
+
* // {
|
|
467
|
+
* // type: "object",
|
|
468
|
+
* // properties: { name: { type: "string", minLength: 2 }, age: { type: "integer" } },
|
|
469
|
+
* // required: ["name"],
|
|
470
|
+
* // }
|
|
471
|
+
*
|
|
472
|
+
* @returns {JSONSchema}
|
|
473
|
+
*/
|
|
474
|
+
toJSONSchema() {
|
|
475
|
+
/** @type {_JSONContext} */
|
|
476
|
+
const ctx = { defs: {}, names: new Map() };
|
|
477
|
+
const out = this._toJSON(ctx);
|
|
478
|
+
if (ctx.names.size) out.$defs = ctx.defs;
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The JSON Schema of the type itself. Subclasses override it.
|
|
484
|
+
* @param {_JSONContext} _ctx
|
|
485
|
+
* @returns {JSONSchema}
|
|
486
|
+
* @protected
|
|
487
|
+
*/
|
|
488
|
+
_json(_ctx) {
|
|
489
|
+
return {};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The JSON Schema with what every schema can carry: rules, `null`,
|
|
494
|
+
* a description, a default.
|
|
495
|
+
* @param {_JSONContext} ctx
|
|
496
|
+
* @returns {JSONSchema}
|
|
497
|
+
* @protected
|
|
498
|
+
*/
|
|
499
|
+
_toJSON(ctx) {
|
|
500
|
+
let out = this._json(ctx);
|
|
501
|
+
for (const step of this._steps) {
|
|
502
|
+
if (step.kind === "check" && step.json) Object.assign(out, step.json);
|
|
503
|
+
}
|
|
504
|
+
if (this._nullable) out = { anyOf: [out, { type: "null" }] };
|
|
505
|
+
if (this._description !== undefined) out.description = this._description;
|
|
506
|
+
if (this._default && typeof this._default.value !== "function") out.default = this._default.value;
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Attaches a description, available as `.description`. Useful to generate
|
|
512
|
+
* docs or forms.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} text
|
|
515
|
+
* @returns {this}
|
|
516
|
+
*/
|
|
517
|
+
describe(text) {
|
|
518
|
+
const copy = this._clone();
|
|
519
|
+
copy._description = text;
|
|
520
|
+
return copy;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The text set with `describe()`. */
|
|
524
|
+
get description() {
|
|
525
|
+
return this._description;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A string schema.
|
|
531
|
+
*
|
|
532
|
+
* @extends {Schema<string>}
|
|
533
|
+
*/
|
|
534
|
+
class StringSchema extends Schema {
|
|
535
|
+
/** @protected */
|
|
536
|
+
_coerce = false;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @param {unknown} value
|
|
540
|
+
* @param {ParseContext} ctx
|
|
541
|
+
* @returns {unknown}
|
|
542
|
+
* @override
|
|
543
|
+
* @protected
|
|
544
|
+
*/
|
|
545
|
+
_check(value, ctx) {
|
|
546
|
+
let v = value;
|
|
547
|
+
if (this._coerce && v !== undefined && v !== null && typeof v !== "string") v = String(v);
|
|
548
|
+
if (typeof v !== "string") return _typeIssue(ctx, "string", value, this._typeMessage);
|
|
549
|
+
return v;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param {(v: string) => boolean} test
|
|
554
|
+
* @param {string} code
|
|
555
|
+
* @param {Message} message
|
|
556
|
+
* @param {Record<string, unknown>} [details]
|
|
557
|
+
* @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
|
|
558
|
+
* @returns {this}
|
|
559
|
+
* @protected
|
|
560
|
+
*/
|
|
561
|
+
_rule(test, code, message, details, json) {
|
|
562
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* At least `length` characters.
|
|
567
|
+
* @param {number} length
|
|
568
|
+
* @param {Message} [message]
|
|
569
|
+
* @returns {this}
|
|
570
|
+
*/
|
|
571
|
+
min(length, message = `Must contain at least ${length} character${length > 1 ? "s" : ""}`) {
|
|
572
|
+
return this._rule((v) => v.length >= length, "too_small", message, { minimum: length }, { minLength: length });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* At most `length` characters.
|
|
577
|
+
* @param {number} length
|
|
578
|
+
* @param {Message} [message]
|
|
579
|
+
* @returns {this}
|
|
580
|
+
*/
|
|
581
|
+
max(length, message = `Must contain at most ${length} character${length > 1 ? "s" : ""}`) {
|
|
582
|
+
return this._rule((v) => v.length <= length, "too_big", message, { maximum: length }, { maxLength: length });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Exactly `length` characters.
|
|
587
|
+
* @param {number} length
|
|
588
|
+
* @param {Message} [message]
|
|
589
|
+
* @returns {this}
|
|
590
|
+
*/
|
|
591
|
+
length(length, message = `Must contain exactly ${length} characters`) {
|
|
592
|
+
return this._rule((v) => v.length === length, "invalid_length", message, { exact: length }, { minLength: length, maxLength: length });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Not the empty string.
|
|
597
|
+
* @param {Message} [message]
|
|
598
|
+
* @returns {this}
|
|
599
|
+
*/
|
|
600
|
+
nonempty(message = "Must not be empty") {
|
|
601
|
+
return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minLength: 1 });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* A valid email address.
|
|
606
|
+
* @param {Message} [message]
|
|
607
|
+
* @returns {this}
|
|
608
|
+
*/
|
|
609
|
+
email(message = "Invalid email address") {
|
|
610
|
+
const { isEmail } = require("./Checker.js");
|
|
611
|
+
return this._rule(isEmail, "invalid_string", message, { validation: "email" }, { format: "email" });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* An absolute URL.
|
|
616
|
+
* @param {Message | { message?: Message, protocols?: string[] }} [options] A message, or allowed protocols (http and https by default).
|
|
617
|
+
* @returns {this}
|
|
618
|
+
*/
|
|
619
|
+
url(options = "Invalid URL") {
|
|
620
|
+
const opts = typeof options === "object" ? options : { message: options };
|
|
621
|
+
const { isURL } = require("./Checker.js");
|
|
622
|
+
return this._rule((v) => isURL(v, { protocols: opts.protocols }), "invalid_string", opts.message ?? "Invalid URL", { validation: "url" }, { format: "uri" });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* A UUID.
|
|
627
|
+
* @param {Message} [message]
|
|
628
|
+
* @returns {this}
|
|
629
|
+
*/
|
|
630
|
+
uuid(message = "Invalid UUID") {
|
|
631
|
+
const { isUUID } = require("./Checker.js");
|
|
632
|
+
return this._rule((v) => isUUID(v), "invalid_string", message, { validation: "uuid" }, { format: "uuid" });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* An IP address.
|
|
637
|
+
* @param {4 | 6 | Message} [versionOrMessage] Only IPv4 or IPv6, or a message.
|
|
638
|
+
* @param {Message} [message]
|
|
639
|
+
* @returns {this}
|
|
640
|
+
*/
|
|
641
|
+
ip(versionOrMessage, message = "Invalid IP address") {
|
|
642
|
+
const version = typeof versionOrMessage === "number" ? versionOrMessage : undefined;
|
|
643
|
+
const { isIP } = require("./Checker.js");
|
|
644
|
+
return this._rule((v) => isIP(v, version), "invalid_string", typeof versionOrMessage === "number" ? message : versionOrMessage ?? message, { validation: "ip" }, version === 4 ? { format: "ipv4" } : version === 6 ? { format: "ipv6" } : undefined);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* An ISO 8601 date or date-time, like `2024-01-31T10:00:00Z`.
|
|
649
|
+
* @param {Message} [message]
|
|
650
|
+
* @returns {this}
|
|
651
|
+
*/
|
|
652
|
+
datetime(message = "Invalid ISO date") {
|
|
653
|
+
const { isISODate } = require("./Checker.js");
|
|
654
|
+
return this._rule(isISODate, "invalid_string", message, { validation: "datetime" }, { format: "date-time" });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Matches a regular expression.
|
|
659
|
+
* @param {RegExp} pattern
|
|
660
|
+
* @param {Message} [message]
|
|
661
|
+
* @returns {this}
|
|
662
|
+
*/
|
|
663
|
+
regex(pattern, message = `Must match ${pattern}`) {
|
|
664
|
+
return this._rule((v) => {
|
|
665
|
+
pattern.lastIndex = 0;
|
|
666
|
+
return pattern.test(v);
|
|
667
|
+
}, "invalid_string", message, { validation: "regex" }, { pattern: pattern.source });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Starts with `prefix`.
|
|
672
|
+
* @param {string} prefix
|
|
673
|
+
* @param {Message} [message]
|
|
674
|
+
* @returns {this}
|
|
675
|
+
*/
|
|
676
|
+
startsWith(prefix, message = `Must start with "${prefix}"`) {
|
|
677
|
+
return this._rule((v) => v.startsWith(prefix), "invalid_string", message);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Ends with `suffix`.
|
|
682
|
+
* @param {string} suffix
|
|
683
|
+
* @param {Message} [message]
|
|
684
|
+
* @returns {this}
|
|
685
|
+
*/
|
|
686
|
+
endsWith(suffix, message = `Must end with "${suffix}"`) {
|
|
687
|
+
return this._rule((v) => v.endsWith(suffix), "invalid_string", message);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Contains `search`.
|
|
692
|
+
* @param {string} search
|
|
693
|
+
* @param {Message} [message]
|
|
694
|
+
* @returns {this}
|
|
695
|
+
*/
|
|
696
|
+
includes(search, message = `Must include "${search}"`) {
|
|
697
|
+
return this._rule((v) => v.includes(search), "invalid_string", message);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Trims whitespace before the checks that follow.
|
|
702
|
+
* @returns {this}
|
|
703
|
+
*/
|
|
704
|
+
trim() {
|
|
705
|
+
return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.trim() });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Lowercases before the checks that follow.
|
|
710
|
+
* @returns {this}
|
|
711
|
+
*/
|
|
712
|
+
toLowerCase() {
|
|
713
|
+
return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toLowerCase() });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Uppercases before the checks that follow.
|
|
718
|
+
* @returns {this}
|
|
719
|
+
*/
|
|
720
|
+
toUpperCase() {
|
|
721
|
+
return this._with({ kind: "transform", fn: (/** @type {string} */ v) => v.toUpperCase() });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A calendar date like `2024-01-31`, without a time.
|
|
726
|
+
* @param {Message} [message]
|
|
727
|
+
* @returns {this}
|
|
728
|
+
*/
|
|
729
|
+
date(message = "Invalid date, expected YYYY-MM-DD") {
|
|
730
|
+
return this._rule((v) => _DATE.test(v) && new Date(`${v}T00:00:00Z`).toISOString().startsWith(v), "invalid_string", message, { validation: "date" }, { format: "date" });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* A URL slug like `my-first-post`: lowercase letters, digits and single dashes.
|
|
735
|
+
* @param {Message} [message]
|
|
736
|
+
* @returns {this}
|
|
737
|
+
*/
|
|
738
|
+
slug(message = "Invalid slug") {
|
|
739
|
+
const { isSlug } = require("./Checker.js");
|
|
740
|
+
return this._rule(isSlug, "invalid_string", message, { validation: "slug" }, { pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Hexadecimal digits only.
|
|
745
|
+
* @param {Message} [message]
|
|
746
|
+
* @returns {this}
|
|
747
|
+
*/
|
|
748
|
+
hex(message = "Invalid hexadecimal string") {
|
|
749
|
+
const { isHex } = require("./Checker.js");
|
|
750
|
+
return this._rule((v) => isHex(v), "invalid_string", message, { validation: "hex" }, { pattern: "^[0-9a-fA-F]+$" });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Base64 data.
|
|
755
|
+
* @param {Message} [message]
|
|
756
|
+
* @returns {this}
|
|
757
|
+
*/
|
|
758
|
+
base64(message = "Invalid Base64 string") {
|
|
759
|
+
const { isBase64 } = require("./Checker.js");
|
|
760
|
+
return this._rule((v) => isBase64(v), "invalid_string", message, { validation: "base64" }, { contentEncoding: "base64" });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* A ULID.
|
|
765
|
+
* @param {Message} [message]
|
|
766
|
+
* @returns {this}
|
|
767
|
+
*/
|
|
768
|
+
ulid(message = "Invalid ULID") {
|
|
769
|
+
const { isULID } = require("./Checker.js");
|
|
770
|
+
return this._rule(isULID, "invalid_string", message, { validation: "ulid" });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* A semantic version like `1.4.0` or `2.0.0-beta.1`.
|
|
775
|
+
* @param {Message} [message]
|
|
776
|
+
* @returns {this}
|
|
777
|
+
*/
|
|
778
|
+
semver(message = "Invalid version") {
|
|
779
|
+
const { isSemver } = require("./Checker.js");
|
|
780
|
+
return this._rule(isSemver, "invalid_string", message, { validation: "semver" });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* A host name like `api.example.com`.
|
|
785
|
+
* @param {Message} [message]
|
|
786
|
+
* @returns {this}
|
|
787
|
+
*/
|
|
788
|
+
hostname(message = "Invalid host name") {
|
|
789
|
+
const { isHostname } = require("./Checker.js");
|
|
790
|
+
return this._rule((v) => isHostname(v), "invalid_string", message, { validation: "hostname" }, { format: "hostname" });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* @returns {JSONSchema}
|
|
795
|
+
* @override
|
|
796
|
+
* @protected
|
|
797
|
+
*/
|
|
798
|
+
_json() {
|
|
799
|
+
return { type: "string" };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* A number schema.
|
|
805
|
+
*
|
|
806
|
+
* @extends {Schema<number>}
|
|
807
|
+
*/
|
|
808
|
+
class NumberSchema extends Schema {
|
|
809
|
+
/** @protected */
|
|
810
|
+
_coerce = false;
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* @param {unknown} value
|
|
814
|
+
* @param {ParseContext} ctx
|
|
815
|
+
* @returns {unknown}
|
|
816
|
+
* @override
|
|
817
|
+
* @protected
|
|
818
|
+
*/
|
|
819
|
+
_check(value, ctx) {
|
|
820
|
+
let v = value;
|
|
821
|
+
if (this._coerce && typeof v === "string" && v.trim() !== "") v = Number(v);
|
|
822
|
+
else if (this._coerce && (typeof v === "boolean" || typeof v === "bigint")) v = Number(v);
|
|
823
|
+
else if (this._coerce && types.isDate(v)) v = v.getTime();
|
|
824
|
+
if (typeof v !== "number" || Number.isNaN(v)) return _typeIssue(ctx, "number", value, this._typeMessage);
|
|
825
|
+
if (!Number.isFinite(v)) {
|
|
826
|
+
_issue(ctx, "not_finite", "Must be a finite number");
|
|
827
|
+
return v;
|
|
828
|
+
}
|
|
829
|
+
return v;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* @param {(v: number) => boolean} test
|
|
834
|
+
* @param {string} code
|
|
835
|
+
* @param {Message} message
|
|
836
|
+
* @param {Record<string, unknown>} [details]
|
|
837
|
+
* @param {Record<string, unknown>} [json] What the rule is called in JSON Schema.
|
|
838
|
+
* @returns {this}
|
|
839
|
+
* @protected
|
|
840
|
+
*/
|
|
841
|
+
_rule(test, code, message, details, json) {
|
|
842
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* At least `value`.
|
|
847
|
+
* @param {number} value
|
|
848
|
+
* @param {Message} [message]
|
|
849
|
+
* @returns {this}
|
|
850
|
+
*/
|
|
851
|
+
min(value, message = `Must be greater than or equal to ${value}`) {
|
|
852
|
+
return this._rule((v) => v >= value, "too_small", message, { minimum: value }, { minimum: value });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* At most `value`.
|
|
857
|
+
* @param {number} value
|
|
858
|
+
* @param {Message} [message]
|
|
859
|
+
* @returns {this}
|
|
860
|
+
*/
|
|
861
|
+
max(value, message = `Must be less than or equal to ${value}`) {
|
|
862
|
+
return this._rule((v) => v <= value, "too_big", message, { maximum: value }, { maximum: value });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* An integer.
|
|
867
|
+
* @param {Message} [message]
|
|
868
|
+
* @returns {this}
|
|
869
|
+
*/
|
|
870
|
+
int(message = "Must be an integer") {
|
|
871
|
+
return this._rule(Number.isInteger, "invalid_type", message, { expected: "integer" }, { type: "integer" });
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Greater than 0.
|
|
876
|
+
* @param {Message} [message]
|
|
877
|
+
* @returns {this}
|
|
878
|
+
*/
|
|
879
|
+
positive(message = "Must be positive") {
|
|
880
|
+
return this._rule((v) => v > 0, "too_small", message, { minimum: 0, exclusive: true }, { exclusiveMinimum: 0 });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* 0 or more.
|
|
885
|
+
* @param {Message} [message]
|
|
886
|
+
* @returns {this}
|
|
887
|
+
*/
|
|
888
|
+
nonnegative(message = "Must not be negative") {
|
|
889
|
+
return this._rule((v) => v >= 0, "too_small", message, { minimum: 0 }, { minimum: 0 });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Less than 0.
|
|
894
|
+
* @param {Message} [message]
|
|
895
|
+
* @returns {this}
|
|
896
|
+
*/
|
|
897
|
+
negative(message = "Must be negative") {
|
|
898
|
+
return this._rule((v) => v < 0, "too_big", message, { maximum: 0, exclusive: true }, { exclusiveMaximum: 0 });
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* A multiple of `step`.
|
|
903
|
+
* @param {number} step
|
|
904
|
+
* @param {Message} [message]
|
|
905
|
+
* @returns {this}
|
|
906
|
+
*/
|
|
907
|
+
multipleOf(step, message = `Must be a multiple of ${step}`) {
|
|
908
|
+
return this._rule((v) => Math.abs(v / step - Math.round(v / step)) < 1e-9, "not_multiple_of", message, { multipleOf: step }, { multipleOf: step });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Greater than `value`, which itself is refused.
|
|
913
|
+
* @param {number} value
|
|
914
|
+
* @param {Message} [message]
|
|
915
|
+
* @returns {this}
|
|
916
|
+
*/
|
|
917
|
+
gt(value, message = `Must be greater than ${value}`) {
|
|
918
|
+
return this._rule((v) => v > value, "too_small", message, { minimum: value, exclusive: true }, { exclusiveMinimum: value });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Less than `value`, which itself is refused.
|
|
923
|
+
* @param {number} value
|
|
924
|
+
* @param {Message} [message]
|
|
925
|
+
* @returns {this}
|
|
926
|
+
*/
|
|
927
|
+
lt(value, message = `Must be less than ${value}`) {
|
|
928
|
+
return this._rule((v) => v < value, "too_big", message, { maximum: value, exclusive: true }, { exclusiveMaximum: value });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* An integer JavaScript can hold exactly: up to 2^53 - 1 either way.
|
|
933
|
+
* Ids from a database or an API should pass this before any arithmetic.
|
|
934
|
+
* @param {Message} [message]
|
|
935
|
+
* @returns {this}
|
|
936
|
+
*/
|
|
937
|
+
safe(message = "Must be a safe integer") {
|
|
938
|
+
return this._rule(Number.isSafeInteger, "invalid_type", message, { expected: "safe integer" }, { type: "integer", minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* @returns {JSONSchema}
|
|
943
|
+
* @override
|
|
944
|
+
* @protected
|
|
945
|
+
*/
|
|
946
|
+
_json() {
|
|
947
|
+
return { type: "number" };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* A boolean schema.
|
|
953
|
+
*
|
|
954
|
+
* @extends {Schema<boolean>}
|
|
955
|
+
*/
|
|
956
|
+
class BooleanSchema extends Schema {
|
|
957
|
+
/** @protected */
|
|
958
|
+
_coerce = false;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* @param {unknown} value
|
|
962
|
+
* @param {ParseContext} ctx
|
|
963
|
+
* @returns {unknown}
|
|
964
|
+
* @override
|
|
965
|
+
* @protected
|
|
966
|
+
*/
|
|
967
|
+
_check(value, ctx) {
|
|
968
|
+
if (typeof value === "boolean") return value;
|
|
969
|
+
if (this._coerce) {
|
|
970
|
+
const text = String(value).trim().toLowerCase();
|
|
971
|
+
if (_TRUE.includes(text)) return true;
|
|
972
|
+
if (_FALSE.includes(text)) return false;
|
|
973
|
+
}
|
|
974
|
+
return _typeIssue(ctx, "boolean", value, this._typeMessage);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* @returns {JSONSchema}
|
|
979
|
+
* @override
|
|
980
|
+
* @protected
|
|
981
|
+
*/
|
|
982
|
+
_json() {
|
|
983
|
+
return { type: "boolean" };
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* A bigint schema.
|
|
989
|
+
*
|
|
990
|
+
* @extends {Schema<bigint>}
|
|
991
|
+
*/
|
|
992
|
+
class BigIntSchema extends Schema {
|
|
993
|
+
/** @protected */
|
|
994
|
+
_coerce = false;
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* @param {unknown} value
|
|
998
|
+
* @param {ParseContext} ctx
|
|
999
|
+
* @returns {unknown}
|
|
1000
|
+
* @override
|
|
1001
|
+
* @protected
|
|
1002
|
+
*/
|
|
1003
|
+
_check(value, ctx) {
|
|
1004
|
+
if (typeof value === "bigint") return value;
|
|
1005
|
+
if (this._coerce && (typeof value === "string" || typeof value === "number")) {
|
|
1006
|
+
try {
|
|
1007
|
+
return BigInt(value);
|
|
1008
|
+
} catch {
|
|
1009
|
+
// fall through to the error
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return _typeIssue(ctx, "bigint", value, this._typeMessage);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* @returns {JSONSchema}
|
|
1017
|
+
* @override
|
|
1018
|
+
* @protected
|
|
1019
|
+
*/
|
|
1020
|
+
_json() {
|
|
1021
|
+
return { type: "integer" };
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* A date schema.
|
|
1027
|
+
*
|
|
1028
|
+
* @extends {Schema<Date>}
|
|
1029
|
+
*/
|
|
1030
|
+
class DateSchema extends Schema {
|
|
1031
|
+
/** @protected */
|
|
1032
|
+
_coerce = false;
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* @param {unknown} value
|
|
1036
|
+
* @param {ParseContext} ctx
|
|
1037
|
+
* @returns {unknown}
|
|
1038
|
+
* @override
|
|
1039
|
+
* @protected
|
|
1040
|
+
*/
|
|
1041
|
+
_check(value, ctx) {
|
|
1042
|
+
let v = value;
|
|
1043
|
+
if (this._coerce && (typeof v === "string" || typeof v === "number")) v = new Date(v);
|
|
1044
|
+
if (!types.isDate(v)) return _typeIssue(ctx, "date", value, this._typeMessage);
|
|
1045
|
+
if (Number.isNaN(v.getTime())) return _issue(ctx, "invalid_date", "Invalid date");
|
|
1046
|
+
return v;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* On or after `date`.
|
|
1051
|
+
* @param {Date | number | string} date
|
|
1052
|
+
* @param {Message} [message]
|
|
1053
|
+
* @returns {this}
|
|
1054
|
+
*/
|
|
1055
|
+
min(date, message) {
|
|
1056
|
+
const limit = new Date(date).getTime();
|
|
1057
|
+
return this._with({ kind: "check", test: (/** @type {Date} */ v) => v.getTime() >= limit, code: "too_small", message: message ?? `Must be on or after ${new Date(limit).toISOString()}` });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* On or before `date`.
|
|
1062
|
+
* @param {Date | number | string} date
|
|
1063
|
+
* @param {Message} [message]
|
|
1064
|
+
* @returns {this}
|
|
1065
|
+
*/
|
|
1066
|
+
max(date, message) {
|
|
1067
|
+
const limit = new Date(date).getTime();
|
|
1068
|
+
return this._with({ kind: "check", test: (/** @type {Date} */ v) => v.getTime() <= limit, code: "too_big", message: message ?? `Must be on or before ${new Date(limit).toISOString()}` });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* @returns {JSONSchema}
|
|
1073
|
+
* @override
|
|
1074
|
+
* @protected
|
|
1075
|
+
*/
|
|
1076
|
+
_json() {
|
|
1077
|
+
return { type: "string", format: "date-time" };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* A schema that accepts a fixed set of values.
|
|
1083
|
+
*
|
|
1084
|
+
* @template T
|
|
1085
|
+
* @extends {Schema<T>}
|
|
1086
|
+
*/
|
|
1087
|
+
class LiteralSchema extends Schema {
|
|
1088
|
+
/** @param {readonly T[]} values */
|
|
1089
|
+
constructor(values) {
|
|
1090
|
+
super();
|
|
1091
|
+
/** @protected */
|
|
1092
|
+
this._values = values;
|
|
1093
|
+
/** @protected @type {Set<unknown>} */
|
|
1094
|
+
this._set = new Set(values);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* @param {unknown} value
|
|
1099
|
+
* @param {ParseContext} ctx
|
|
1100
|
+
* @returns {unknown}
|
|
1101
|
+
* @override
|
|
1102
|
+
* @protected
|
|
1103
|
+
*/
|
|
1104
|
+
_check(value, ctx) {
|
|
1105
|
+
// A Set treats 0 and -0 as the same value: tell them apart, as before.
|
|
1106
|
+
if (this._set.has(value) && (value !== 0 || this._values.some((v) => Object.is(v, value)))) return value;
|
|
1107
|
+
const list = this._values.map((v) => JSON.stringify(v)).join(" | ");
|
|
1108
|
+
const code = this._values.length > 1 ? "invalid_enum_value" : "invalid_literal";
|
|
1109
|
+
return _issue(ctx, code, this._typeMessage ? _message(this._typeMessage, value) : `Expected ${list}, received ${JSON.stringify(value) ?? String(value)}`, { expected: list, received: _typeName(value) });
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* @returns {JSONSchema}
|
|
1114
|
+
* @override
|
|
1115
|
+
* @protected
|
|
1116
|
+
*/
|
|
1117
|
+
_json() {
|
|
1118
|
+
const values = this._values.map((v) => (typeof v === "bigint" ? Number(v) : v === undefined ? null : v));
|
|
1119
|
+
return values.length === 1 ? { const: values[0] } : values.every((v) => typeof v === "string") ? { type: "string", enum: values } : { enum: values };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/** The accepted values. */
|
|
1123
|
+
get options() {
|
|
1124
|
+
return [...this._values];
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* A schema built from a test function.
|
|
1130
|
+
*
|
|
1131
|
+
* @template T
|
|
1132
|
+
* @extends {Schema<T>}
|
|
1133
|
+
*/
|
|
1134
|
+
class CustomSchema extends Schema {
|
|
1135
|
+
/**
|
|
1136
|
+
* @param {(value: unknown) => boolean} test
|
|
1137
|
+
* @param {Message} message
|
|
1138
|
+
* @param {string} expected
|
|
1139
|
+
*/
|
|
1140
|
+
constructor(test, message, expected) {
|
|
1141
|
+
super();
|
|
1142
|
+
/** @protected */
|
|
1143
|
+
this._test = test;
|
|
1144
|
+
/** @protected */
|
|
1145
|
+
this._message = message;
|
|
1146
|
+
/** @protected */
|
|
1147
|
+
this._expected = expected;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* @param {unknown} value
|
|
1152
|
+
* @param {ParseContext} ctx
|
|
1153
|
+
* @returns {unknown}
|
|
1154
|
+
* @override
|
|
1155
|
+
* @protected
|
|
1156
|
+
*/
|
|
1157
|
+
_check(value, ctx) {
|
|
1158
|
+
if (this._test(value)) return value;
|
|
1159
|
+
return _issue(ctx, this._expected === "custom" ? "custom" : "invalid_type", _message(this._message, value), { expected: this._expected, received: _typeName(value) });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* An array schema.
|
|
1165
|
+
*
|
|
1166
|
+
* @template T
|
|
1167
|
+
* @extends {Schema<T[]>}
|
|
1168
|
+
*/
|
|
1169
|
+
class ArraySchema extends Schema {
|
|
1170
|
+
/** @param {Schema<T>} item */
|
|
1171
|
+
constructor(item) {
|
|
1172
|
+
super();
|
|
1173
|
+
/** @protected */
|
|
1174
|
+
this._item = item;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* @param {unknown} value
|
|
1179
|
+
* @param {ParseContext} ctx
|
|
1180
|
+
* @returns {unknown}
|
|
1181
|
+
* @override
|
|
1182
|
+
* @protected
|
|
1183
|
+
*/
|
|
1184
|
+
_check(value, ctx) {
|
|
1185
|
+
if (!Array.isArray(value)) return _typeIssue(ctx, "array", value, this._typeMessage);
|
|
1186
|
+
const item = this._item;
|
|
1187
|
+
const path = ctx.path;
|
|
1188
|
+
const out = new Array(value.length);
|
|
1189
|
+
for (let i = 0; i < value.length; i++) {
|
|
1190
|
+
path.push(i);
|
|
1191
|
+
out[i] = _runChild(item, value[i], ctx);
|
|
1192
|
+
path.pop();
|
|
1193
|
+
}
|
|
1194
|
+
return out;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* @param {(v: T[]) => boolean} test
|
|
1199
|
+
* @param {string} code
|
|
1200
|
+
* @param {Message} message
|
|
1201
|
+
* @param {Record<string, unknown>} [details]
|
|
1202
|
+
* @param {Record<string, unknown>} [json]
|
|
1203
|
+
* @returns {this}
|
|
1204
|
+
* @protected
|
|
1205
|
+
*/
|
|
1206
|
+
_rule(test, code, message, details, json) {
|
|
1207
|
+
return this._with({ kind: "check", test, code, message, details, json });
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* At least `count` items.
|
|
1212
|
+
* @param {number} count
|
|
1213
|
+
* @param {Message} [message]
|
|
1214
|
+
* @returns {this}
|
|
1215
|
+
*/
|
|
1216
|
+
min(count, message = `Must contain at least ${count} item${count > 1 ? "s" : ""}`) {
|
|
1217
|
+
return this._rule((v) => v.length >= count, "too_small", message, { minimum: count }, { minItems: count });
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* At most `count` items.
|
|
1222
|
+
* @param {number} count
|
|
1223
|
+
* @param {Message} [message]
|
|
1224
|
+
* @returns {this}
|
|
1225
|
+
*/
|
|
1226
|
+
max(count, message = `Must contain at most ${count} item${count > 1 ? "s" : ""}`) {
|
|
1227
|
+
return this._rule((v) => v.length <= count, "too_big", message, { maximum: count }, { maxItems: count });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Exactly `count` items.
|
|
1232
|
+
* @param {number} count
|
|
1233
|
+
* @param {Message} [message]
|
|
1234
|
+
* @returns {this}
|
|
1235
|
+
*/
|
|
1236
|
+
length(count, message = `Must contain exactly ${count} items`) {
|
|
1237
|
+
return this._rule((v) => v.length === count, "invalid_length", message, { exact: count }, { minItems: count, maxItems: count });
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* At least one item.
|
|
1242
|
+
* @param {Message} [message]
|
|
1243
|
+
* @returns {this}
|
|
1244
|
+
*/
|
|
1245
|
+
nonempty(message = "Must not be empty") {
|
|
1246
|
+
return this._rule((v) => v.length > 0, "too_small", message, { minimum: 1 }, { minItems: 1 });
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* No duplicates, compared by value or by a key.
|
|
1251
|
+
* @param {((item: T) => unknown) | Message} [keyOrMessage] A function returning the key, or a message.
|
|
1252
|
+
* @param {Message} [message]
|
|
1253
|
+
* @returns {this}
|
|
1254
|
+
*/
|
|
1255
|
+
unique(keyOrMessage, message = "Must not contain duplicates") {
|
|
1256
|
+
const byKey = typeof keyOrMessage === "function" && keyOrMessage.length === 1 ? /** @type {(item: T) => unknown} */ (keyOrMessage) : undefined;
|
|
1257
|
+
const msg = typeof keyOrMessage === "string" ? keyOrMessage : message;
|
|
1258
|
+
return this._rule((v) => new Set(byKey ? v.map(byKey) : v).size === v.length, "not_unique", msg, undefined, byKey ? undefined : { uniqueItems: true });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* @param {_JSONContext} ctx
|
|
1263
|
+
* @returns {JSONSchema}
|
|
1264
|
+
* @override
|
|
1265
|
+
* @protected
|
|
1266
|
+
*/
|
|
1267
|
+
_json(ctx) {
|
|
1268
|
+
return { type: "array", items: _jsonOf(this._item, ctx) };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/** The schema of the items. */
|
|
1272
|
+
get element() {
|
|
1273
|
+
return this._item;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* An object schema.
|
|
1279
|
+
*
|
|
1280
|
+
* @template {Shape} S
|
|
1281
|
+
* @extends {Schema<ObjectOutput<S>>}
|
|
1282
|
+
*/
|
|
1283
|
+
class ObjectSchema extends Schema {
|
|
1284
|
+
/** @protected @type {"strip" | "strict" | "passthrough"} */
|
|
1285
|
+
_unknown = "strip";
|
|
1286
|
+
|
|
1287
|
+
/** @param {S} shape */
|
|
1288
|
+
constructor(shape) {
|
|
1289
|
+
super();
|
|
1290
|
+
/** @protected */
|
|
1291
|
+
this._shape = shape;
|
|
1292
|
+
// Read once here rather than on every parse. A key named __proto__ could
|
|
1293
|
+
// only ever replace the prototype of the result, so it's left out.
|
|
1294
|
+
/** @protected @type {string[]} */
|
|
1295
|
+
this._keys = Object.keys(shape).filter((key) => key !== "__proto__");
|
|
1296
|
+
/** @protected @type {Array<Schema<any>>} */
|
|
1297
|
+
this._schemas = this._keys.map((key) => shape[key]);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* @param {unknown} value
|
|
1302
|
+
* @param {ParseContext} ctx
|
|
1303
|
+
* @returns {unknown}
|
|
1304
|
+
* @override
|
|
1305
|
+
* @protected
|
|
1306
|
+
*/
|
|
1307
|
+
_check(value, ctx) {
|
|
1308
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1309
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1310
|
+
const keys = this._keys;
|
|
1311
|
+
const schemas = this._schemas;
|
|
1312
|
+
const path = ctx.path;
|
|
1313
|
+
/** @type {Record<string, unknown>} */
|
|
1314
|
+
const out = {};
|
|
1315
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1316
|
+
const key = keys[i];
|
|
1317
|
+
let raw = input[key];
|
|
1318
|
+
// What every object inherits, like toString, is not data.
|
|
1319
|
+
if (typeof raw === "function" && !Object.hasOwn(input, key)) raw = undefined;
|
|
1320
|
+
path.push(key);
|
|
1321
|
+
const result = _runChild(schemas[i], raw, ctx);
|
|
1322
|
+
path.pop();
|
|
1323
|
+
if (result !== undefined || Object.hasOwn(input, key)) out[key] = result;
|
|
1324
|
+
}
|
|
1325
|
+
if (this._unknown === "strip") return out;
|
|
1326
|
+
/** @type {string[] | undefined} */
|
|
1327
|
+
let unknown;
|
|
1328
|
+
for (const key of Object.keys(input)) {
|
|
1329
|
+
if (Object.hasOwn(this._shape, key)) continue;
|
|
1330
|
+
if (this._unknown === "strict") (unknown ??= []).push(key);
|
|
1331
|
+
else if (!UNSAFE_KEYS.has(key)) out[key] = input[key];
|
|
1332
|
+
}
|
|
1333
|
+
if (unknown) _issue(ctx, "unrecognized_keys", `Unrecognized key${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}`, { keys: unknown });
|
|
1334
|
+
return out;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* A schema with another shape that keeps how this one treats unknown keys.
|
|
1339
|
+
* @param {Shape} shape
|
|
1340
|
+
* @returns {any}
|
|
1341
|
+
* @protected
|
|
1342
|
+
*/
|
|
1343
|
+
_reshape(shape) {
|
|
1344
|
+
const copy = new ObjectSchema(shape);
|
|
1345
|
+
copy._unknown = this._unknown;
|
|
1346
|
+
return copy;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* @param {_JSONContext} ctx
|
|
1351
|
+
* @returns {JSONSchema}
|
|
1352
|
+
* @override
|
|
1353
|
+
* @protected
|
|
1354
|
+
*/
|
|
1355
|
+
_json(ctx) {
|
|
1356
|
+
/** @type {Record<string, JSONSchema>} */
|
|
1357
|
+
const properties = {};
|
|
1358
|
+
const required = [];
|
|
1359
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
1360
|
+
const child = /** @type {any} */ (this._schemas[i]);
|
|
1361
|
+
properties[this._keys[i]] = _jsonOf(child, ctx);
|
|
1362
|
+
// any() and unknown() take undefined too, so a key of theirs can be left out
|
|
1363
|
+
if (!child._optional && !child._default && child.constructor !== Schema) required.push(this._keys[i]);
|
|
1364
|
+
}
|
|
1365
|
+
/** @type {JSONSchema} */
|
|
1366
|
+
const out = { type: "object", properties };
|
|
1367
|
+
if (required.length) out.required = required;
|
|
1368
|
+
if (this._unknown === "strict") out.additionalProperties = false;
|
|
1369
|
+
return out;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/** The schema of each key. */
|
|
1373
|
+
get shape() {
|
|
1374
|
+
return this._shape;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Rejects unknown keys instead of dropping them.
|
|
1379
|
+
* @returns {this}
|
|
1380
|
+
*/
|
|
1381
|
+
strict() {
|
|
1382
|
+
const copy = this._clone();
|
|
1383
|
+
copy._unknown = "strict";
|
|
1384
|
+
return copy;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Keeps unknown keys instead of dropping them.
|
|
1389
|
+
* @returns {this}
|
|
1390
|
+
*/
|
|
1391
|
+
passthrough() {
|
|
1392
|
+
const copy = this._clone();
|
|
1393
|
+
copy._unknown = "passthrough";
|
|
1394
|
+
return copy;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Adds keys, or replaces existing ones.
|
|
1399
|
+
* @template {Shape} M
|
|
1400
|
+
* @param {M} more
|
|
1401
|
+
* @returns {ObjectSchema<Omit<S, keyof M> & M>}
|
|
1402
|
+
*/
|
|
1403
|
+
extend(more) {
|
|
1404
|
+
return this._reshape({ ...this._shape, ...more });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Keeps only some keys.
|
|
1409
|
+
* @template {keyof S & string} K
|
|
1410
|
+
* @param {readonly K[]} keys
|
|
1411
|
+
* @returns {ObjectSchema<Pick<S, K>>}
|
|
1412
|
+
*/
|
|
1413
|
+
pick(keys) {
|
|
1414
|
+
/** @type {any} */
|
|
1415
|
+
const shape = {};
|
|
1416
|
+
for (const key of keys) shape[key] = this._shape[key];
|
|
1417
|
+
return this._reshape(shape);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Drops some keys.
|
|
1422
|
+
* @template {keyof S & string} K
|
|
1423
|
+
* @param {readonly K[]} keys
|
|
1424
|
+
* @returns {ObjectSchema<Omit<S, K>>}
|
|
1425
|
+
*/
|
|
1426
|
+
omit(keys) {
|
|
1427
|
+
/** @type {any} */
|
|
1428
|
+
const shape = { ...this._shape };
|
|
1429
|
+
for (const key of keys) delete shape[key];
|
|
1430
|
+
return this._reshape(shape);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Makes every key optional, which suits PATCH requests.
|
|
1435
|
+
* @returns {ObjectSchema<{ [K in keyof S]: Schema<Infer<S[K]> | undefined> }>}
|
|
1436
|
+
*/
|
|
1437
|
+
partial() {
|
|
1438
|
+
/** @type {any} */
|
|
1439
|
+
const shape = {};
|
|
1440
|
+
for (let i = 0; i < this._keys.length; i++) shape[this._keys[i]] = this._schemas[i].optional();
|
|
1441
|
+
return this._reshape(shape);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Makes every key required again: the opposite of `partial()`.
|
|
1446
|
+
* @returns {ObjectSchema<{ [K in keyof S]: Schema<Exclude<Infer<S[K]>, undefined>> }>}
|
|
1447
|
+
*/
|
|
1448
|
+
required() {
|
|
1449
|
+
/** @type {any} */
|
|
1450
|
+
const shape = {};
|
|
1451
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
1452
|
+
const copy = /** @type {any} */ (this._schemas[i])._clone();
|
|
1453
|
+
copy._optional = false;
|
|
1454
|
+
shape[this._keys[i]] = copy;
|
|
1455
|
+
}
|
|
1456
|
+
return this._reshape(shape);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Combines two object schemas. Keys of the other one win, and so does
|
|
1461
|
+
* its way of treating unknown keys.
|
|
1462
|
+
*
|
|
1463
|
+
* @example
|
|
1464
|
+
* const Timestamps = s.object({ createdAt: s.date(), updatedAt: s.date() });
|
|
1465
|
+
* const Post = s.object({ title: s.string() }).merge(Timestamps);
|
|
1466
|
+
*
|
|
1467
|
+
* @template {Shape} M
|
|
1468
|
+
* @param {ObjectSchema<M>} other
|
|
1469
|
+
* @returns {ObjectSchema<Omit<S, keyof M> & M>}
|
|
1470
|
+
*/
|
|
1471
|
+
merge(other) {
|
|
1472
|
+
const copy = /** @type {any} */ (new ObjectSchema({ ...this._shape, ...other._shape }));
|
|
1473
|
+
copy._unknown = other._unknown;
|
|
1474
|
+
return copy;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* A schema that accepts the name of any key: handy for a `sort` or
|
|
1479
|
+
* `fields` query parameter.
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* const SortBy = User.keyof(); // "name" | "email" | "age" | "role"
|
|
1483
|
+
*
|
|
1484
|
+
* @returns {LiteralSchema<keyof S & string>}
|
|
1485
|
+
*/
|
|
1486
|
+
keyof() {
|
|
1487
|
+
return new LiteralSchema(/** @type {Array<keyof S & string>} */ (this._keys));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* A schema that accepts any of several schemas.
|
|
1493
|
+
*
|
|
1494
|
+
* @template T
|
|
1495
|
+
* @extends {Schema<T>}
|
|
1496
|
+
*/
|
|
1497
|
+
class UnionSchema extends Schema {
|
|
1498
|
+
/** @param {ReadonlyArray<Schema<any>>} options */
|
|
1499
|
+
constructor(options) {
|
|
1500
|
+
super();
|
|
1501
|
+
/** @protected */
|
|
1502
|
+
this._options = options;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* @param {unknown} value
|
|
1507
|
+
* @param {ParseContext} ctx
|
|
1508
|
+
* @returns {unknown}
|
|
1509
|
+
* @override
|
|
1510
|
+
* @protected
|
|
1511
|
+
*/
|
|
1512
|
+
_check(value, ctx) {
|
|
1513
|
+
/** @type {import("./errors").ValidationIssue[][]} */
|
|
1514
|
+
const failures = [];
|
|
1515
|
+
for (const option of this._options) {
|
|
1516
|
+
const attempt = { issues: [], path: ctx.path };
|
|
1517
|
+
const out = _runChild(option, value, attempt);
|
|
1518
|
+
if (!attempt.issues.length) return out;
|
|
1519
|
+
failures.push(attempt.issues);
|
|
1520
|
+
}
|
|
1521
|
+
const typeOnly = failures.every((issues) => issues.length === 1 && issues[0].code === "invalid_type" && issues[0].path.length === ctx.path.length);
|
|
1522
|
+
if (typeOnly) {
|
|
1523
|
+
const expected = failures.map((issues) => issues[0].expected).join(" | ");
|
|
1524
|
+
return _issue(ctx, "invalid_union", `Expected ${expected}, received ${_typeName(value)}`, { expected, received: _typeName(value) });
|
|
1525
|
+
}
|
|
1526
|
+
const best = failures.reduce((a, b) => (b.length < a.length ? b : a));
|
|
1527
|
+
ctx.issues.push(...best);
|
|
1528
|
+
return INVALID;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* @param {_JSONContext} ctx
|
|
1533
|
+
* @returns {JSONSchema}
|
|
1534
|
+
* @override
|
|
1535
|
+
* @protected
|
|
1536
|
+
*/
|
|
1537
|
+
_json(ctx) {
|
|
1538
|
+
return { anyOf: this._options.map((option) => _jsonOf(option, ctx)) };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* A union of objects told apart by one key.
|
|
1544
|
+
*
|
|
1545
|
+
* @template T
|
|
1546
|
+
* @extends {Schema<T>}
|
|
1547
|
+
*/
|
|
1548
|
+
class DiscriminatedUnionSchema extends Schema {
|
|
1549
|
+
/**
|
|
1550
|
+
* @param {string} key
|
|
1551
|
+
* @param {ReadonlyArray<ObjectSchema<any>>} options
|
|
1552
|
+
*/
|
|
1553
|
+
constructor(key, options) {
|
|
1554
|
+
super();
|
|
1555
|
+
/** @protected */
|
|
1556
|
+
this._key = key;
|
|
1557
|
+
/** @protected */
|
|
1558
|
+
this._options = options;
|
|
1559
|
+
/** @protected @type {Map<unknown, Schema<any>>} */
|
|
1560
|
+
this._byValue = new Map();
|
|
1561
|
+
for (const option of options) {
|
|
1562
|
+
const values = /** @type {any} */ (option)?._shape?.[key]?._values;
|
|
1563
|
+
if (!Array.isArray(values)) throw new TypeError(`discriminatedUnion(): every option must be an object with a literal "${key}"`);
|
|
1564
|
+
for (const value of values) {
|
|
1565
|
+
if (this._byValue.has(value)) throw new TypeError(`discriminatedUnion(): two options have "${key}" set to ${JSON.stringify(value)}`);
|
|
1566
|
+
this._byValue.set(value, option);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* @param {unknown} value
|
|
1573
|
+
* @param {ParseContext} ctx
|
|
1574
|
+
* @returns {unknown}
|
|
1575
|
+
* @override
|
|
1576
|
+
* @protected
|
|
1577
|
+
*/
|
|
1578
|
+
_check(value, ctx) {
|
|
1579
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1580
|
+
const tag = /** @type {Record<string, unknown>} */ (value)[this._key];
|
|
1581
|
+
const option = this._byValue.get(tag);
|
|
1582
|
+
if (option) return _runChild(option, value, ctx);
|
|
1583
|
+
const expected = [...this._byValue.keys()].map((known) => JSON.stringify(known)).join(" | ");
|
|
1584
|
+
ctx.path.push(this._key);
|
|
1585
|
+
_issue(ctx, "invalid_union_discriminator", `Expected ${expected}, received ${JSON.stringify(tag) ?? String(tag)}`, { expected, received: _typeName(tag) });
|
|
1586
|
+
ctx.path.pop();
|
|
1587
|
+
return INVALID;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* @param {_JSONContext} ctx
|
|
1592
|
+
* @returns {JSONSchema}
|
|
1593
|
+
* @override
|
|
1594
|
+
* @protected
|
|
1595
|
+
*/
|
|
1596
|
+
_json(ctx) {
|
|
1597
|
+
return { oneOf: this._options.map((option) => _jsonOf(option, ctx)), discriminator: { propertyName: this._key } };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* A fixed-length array schema.
|
|
1603
|
+
*
|
|
1604
|
+
* @template {any[]} T
|
|
1605
|
+
* @extends {Schema<T>}
|
|
1606
|
+
*/
|
|
1607
|
+
class TupleSchema extends Schema {
|
|
1608
|
+
/** @param {ReadonlyArray<Schema<any>>} items */
|
|
1609
|
+
constructor(items) {
|
|
1610
|
+
super();
|
|
1611
|
+
/** @protected */
|
|
1612
|
+
this._items = items;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* @param {unknown} value
|
|
1617
|
+
* @param {ParseContext} ctx
|
|
1618
|
+
* @returns {unknown}
|
|
1619
|
+
* @override
|
|
1620
|
+
* @protected
|
|
1621
|
+
*/
|
|
1622
|
+
_check(value, ctx) {
|
|
1623
|
+
if (!Array.isArray(value)) return _typeIssue(ctx, "tuple", value, this._typeMessage);
|
|
1624
|
+
if (value.length !== this._items.length) {
|
|
1625
|
+
return _issue(ctx, "invalid_length", `Expected ${this._items.length} items, received ${value.length}`);
|
|
1626
|
+
}
|
|
1627
|
+
const path = ctx.path;
|
|
1628
|
+
const out = new Array(this._items.length);
|
|
1629
|
+
for (let i = 0; i < out.length; i++) {
|
|
1630
|
+
path.push(i);
|
|
1631
|
+
out[i] = _runChild(this._items[i], value[i], ctx);
|
|
1632
|
+
path.pop();
|
|
1633
|
+
}
|
|
1634
|
+
return out;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* @param {_JSONContext} ctx
|
|
1639
|
+
* @returns {JSONSchema}
|
|
1640
|
+
* @override
|
|
1641
|
+
* @protected
|
|
1642
|
+
*/
|
|
1643
|
+
_json(ctx) {
|
|
1644
|
+
return { type: "array", prefixItems: this._items.map((item) => _jsonOf(item, ctx)), items: false, minItems: this._items.length, maxItems: this._items.length };
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* A dictionary schema.
|
|
1650
|
+
*
|
|
1651
|
+
* @template V
|
|
1652
|
+
* @extends {Schema<Record<string, V>>}
|
|
1653
|
+
*/
|
|
1654
|
+
class RecordSchema extends Schema {
|
|
1655
|
+
/**
|
|
1656
|
+
* @param {Schema<V>} values
|
|
1657
|
+
* @param {Schema<string> | undefined} keys
|
|
1658
|
+
*/
|
|
1659
|
+
constructor(values, keys) {
|
|
1660
|
+
super();
|
|
1661
|
+
/** @protected */
|
|
1662
|
+
this._values = values;
|
|
1663
|
+
/** @protected */
|
|
1664
|
+
this._keys = keys;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* @param {unknown} value
|
|
1669
|
+
* @param {ParseContext} ctx
|
|
1670
|
+
* @returns {unknown}
|
|
1671
|
+
* @override
|
|
1672
|
+
* @protected
|
|
1673
|
+
*/
|
|
1674
|
+
_check(value, ctx) {
|
|
1675
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return _typeIssue(ctx, "object", value, this._typeMessage);
|
|
1676
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1677
|
+
const path = ctx.path;
|
|
1678
|
+
/** @type {Record<string, unknown>} */
|
|
1679
|
+
const out = {};
|
|
1680
|
+
for (const key of Object.keys(input)) {
|
|
1681
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
1682
|
+
path.push(key);
|
|
1683
|
+
const k = this._keys ? _runChild(this._keys, key, ctx) : key;
|
|
1684
|
+
const v = _runChild(this._values, input[key], ctx);
|
|
1685
|
+
path.pop();
|
|
1686
|
+
if (typeof k === "string" && !UNSAFE_KEYS.has(k)) out[k] = v;
|
|
1687
|
+
}
|
|
1688
|
+
return out;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* @param {_JSONContext} ctx
|
|
1693
|
+
* @returns {JSONSchema}
|
|
1694
|
+
* @override
|
|
1695
|
+
* @protected
|
|
1696
|
+
*/
|
|
1697
|
+
_json(ctx) {
|
|
1698
|
+
/** @type {JSONSchema} */
|
|
1699
|
+
const out = { type: "object", additionalProperties: _jsonOf(this._values, ctx) };
|
|
1700
|
+
if (this._keys) out.propertyNames = _jsonOf(this._keys, ctx);
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* A schema resolved on first use, for recursive data.
|
|
1707
|
+
*
|
|
1708
|
+
* @template T
|
|
1709
|
+
* @extends {Schema<T>}
|
|
1710
|
+
*/
|
|
1711
|
+
class LazySchema extends Schema {
|
|
1712
|
+
/** @param {() => Schema<T>} getter */
|
|
1713
|
+
constructor(getter) {
|
|
1714
|
+
super();
|
|
1715
|
+
/** @protected */
|
|
1716
|
+
this._getter = getter;
|
|
1717
|
+
/** @protected @type {Schema<T> | undefined} */
|
|
1718
|
+
this._resolved = undefined;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* @param {unknown} value
|
|
1723
|
+
* @param {ParseContext} ctx
|
|
1724
|
+
* @returns {unknown}
|
|
1725
|
+
* @override
|
|
1726
|
+
* @protected
|
|
1727
|
+
*/
|
|
1728
|
+
_check(value, ctx) {
|
|
1729
|
+
const before = ctx.issues.length;
|
|
1730
|
+
const out = _runChild((this._resolved ??= this._getter()), value, ctx);
|
|
1731
|
+
return ctx.issues.length > before ? INVALID : out;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* A schema that refers to itself can't be written inline: it goes under
|
|
1736
|
+
* `$defs` and is pointed at.
|
|
1737
|
+
* @param {_JSONContext} ctx
|
|
1738
|
+
* @returns {JSONSchema}
|
|
1739
|
+
* @override
|
|
1740
|
+
* @protected
|
|
1741
|
+
*/
|
|
1742
|
+
_json(ctx) {
|
|
1743
|
+
const target = (this._resolved ??= this._getter());
|
|
1744
|
+
let name = ctx.names.get(target);
|
|
1745
|
+
if (!name) {
|
|
1746
|
+
name = `schema${ctx.names.size + 1}`;
|
|
1747
|
+
ctx.names.set(target, name);
|
|
1748
|
+
ctx.defs[name] = _jsonOf(target, ctx);
|
|
1749
|
+
}
|
|
1750
|
+
return { $ref: `#/$defs/${name}` };
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* A string schema.
|
|
1756
|
+
*
|
|
1757
|
+
* @example
|
|
1758
|
+
* s.string().trim().min(1).max(100);
|
|
1759
|
+
* s.string().email();
|
|
1760
|
+
*
|
|
1761
|
+
* @param {{ message?: Message }} [options] Message when the value isn't a string.
|
|
1762
|
+
* @returns {StringSchema}
|
|
1763
|
+
*/
|
|
1764
|
+
function string(options = {}) {
|
|
1765
|
+
const schema = new StringSchema();
|
|
1766
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1767
|
+
return schema;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* A number. `NaN` and `Infinity` are refused.
|
|
1772
|
+
*
|
|
1773
|
+
* @example
|
|
1774
|
+
* s.number().int().min(0).max(120);
|
|
1775
|
+
*
|
|
1776
|
+
* @param {{ message?: Message }} [options] Message when the value isn't a number.
|
|
1777
|
+
* @returns {NumberSchema}
|
|
1778
|
+
*/
|
|
1779
|
+
function number(options = {}) {
|
|
1780
|
+
const schema = new NumberSchema();
|
|
1781
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1782
|
+
return schema;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* A boolean schema.
|
|
1787
|
+
*
|
|
1788
|
+
* @param {{ message?: Message }} [options]
|
|
1789
|
+
* @returns {BooleanSchema}
|
|
1790
|
+
*/
|
|
1791
|
+
function boolean(options = {}) {
|
|
1792
|
+
const schema = new BooleanSchema();
|
|
1793
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1794
|
+
return schema;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* A bigint schema.
|
|
1799
|
+
*
|
|
1800
|
+
* @param {{ message?: Message }} [options]
|
|
1801
|
+
* @returns {BigIntSchema}
|
|
1802
|
+
*/
|
|
1803
|
+
function bigint(options = {}) {
|
|
1804
|
+
const schema = new BigIntSchema();
|
|
1805
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1806
|
+
return schema;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* A valid `Date`.
|
|
1811
|
+
*
|
|
1812
|
+
* @example
|
|
1813
|
+
* s.date().min("2024-01-01");
|
|
1814
|
+
*
|
|
1815
|
+
* @param {{ message?: Message }} [options]
|
|
1816
|
+
* @returns {DateSchema}
|
|
1817
|
+
*/
|
|
1818
|
+
function date(options = {}) {
|
|
1819
|
+
const schema = new DateSchema();
|
|
1820
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1821
|
+
return schema;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Exactly this value.
|
|
1826
|
+
*
|
|
1827
|
+
* @example
|
|
1828
|
+
* s.object({ type: s.literal("circle"), radius: s.number() });
|
|
1829
|
+
*
|
|
1830
|
+
* @template {string | number | boolean | bigint | null | undefined} L
|
|
1831
|
+
* @param {L} value
|
|
1832
|
+
* @returns {LiteralSchema<L>}
|
|
1833
|
+
*/
|
|
1834
|
+
function literal(value) {
|
|
1835
|
+
return new LiteralSchema([value]);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* One of these values. The type is their union.
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* const Role = s.enum(["admin", "editor", "viewer"]); // Schema<"admin" | "editor" | "viewer">
|
|
1843
|
+
* Role.options; // ["admin", "editor", "viewer"]
|
|
1844
|
+
*
|
|
1845
|
+
* @template {string | number} V
|
|
1846
|
+
* @param {readonly V[]} values
|
|
1847
|
+
* @param {{ message?: Message }} [options]
|
|
1848
|
+
* @returns {LiteralSchema<V>}
|
|
1849
|
+
*/
|
|
1850
|
+
function enumeration(values, options = {}) {
|
|
1851
|
+
const schema = new LiteralSchema(values);
|
|
1852
|
+
/** @type {any} */ (schema)._typeMessage = options.message;
|
|
1853
|
+
return schema;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* An array whose items all match `item`.
|
|
1858
|
+
*
|
|
1859
|
+
* @example
|
|
1860
|
+
* s.array(s.string()).min(1).max(10).unique();
|
|
1861
|
+
*
|
|
1862
|
+
* @template T
|
|
1863
|
+
* @param {Schema<T>} item
|
|
1864
|
+
* @returns {ArraySchema<T>}
|
|
1865
|
+
*/
|
|
1866
|
+
function array(item) {
|
|
1867
|
+
return new ArraySchema(item);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* An object with known keys. Unknown keys are dropped; `.strict()` rejects
|
|
1872
|
+
* them and `.passthrough()` keeps them.
|
|
1873
|
+
*
|
|
1874
|
+
* @example
|
|
1875
|
+
* const Address = s.object({ street: s.string(), zip: s.string().regex(/^\d{5}$/), city: s.string() });
|
|
1876
|
+
*
|
|
1877
|
+
* @template {Shape} S
|
|
1878
|
+
* @param {S} shape
|
|
1879
|
+
* @returns {ObjectSchema<S>}
|
|
1880
|
+
*/
|
|
1881
|
+
function object(shape) {
|
|
1882
|
+
return new ObjectSchema(shape);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Matches any of these schemas; the first that fits wins.
|
|
1887
|
+
*
|
|
1888
|
+
* @example
|
|
1889
|
+
* s.union([s.string(), s.number()]); // Schema<string | number>
|
|
1890
|
+
*
|
|
1891
|
+
* @template {ReadonlyArray<Schema<any>>} U
|
|
1892
|
+
* @param {U} options
|
|
1893
|
+
* @returns {UnionSchema<Infer<U[number]>>}
|
|
1894
|
+
*/
|
|
1895
|
+
function union(options) {
|
|
1896
|
+
return new UnionSchema(options);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* A union of objects told apart by one key, like the `type` of an event.
|
|
1901
|
+
* Unlike `union()`, it goes straight to the right schema, and a wrong
|
|
1902
|
+
* object gets the errors of the kind it claims to be.
|
|
1903
|
+
*
|
|
1904
|
+
* @example
|
|
1905
|
+
* const Event = s.discriminatedUnion("type", [
|
|
1906
|
+
* s.object({ type: s.literal("click"), x: s.number(), y: s.number() }),
|
|
1907
|
+
* s.object({ type: s.literal("key"), key: s.string() }),
|
|
1908
|
+
* ]);
|
|
1909
|
+
* Event.parse({ type: "key", key: "Enter" });
|
|
1910
|
+
*
|
|
1911
|
+
* @template {ReadonlyArray<ObjectSchema<any>>} U
|
|
1912
|
+
* @param {string} key The key every option has, with a different literal in each.
|
|
1913
|
+
* @param {U} options
|
|
1914
|
+
* @returns {DiscriminatedUnionSchema<Infer<U[number]>>}
|
|
1915
|
+
*/
|
|
1916
|
+
function discriminatedUnion(key, options) {
|
|
1917
|
+
return new DiscriminatedUnionSchema(key, options);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* A fixed-length array with one schema per position.
|
|
1922
|
+
*
|
|
1923
|
+
* @example
|
|
1924
|
+
* s.tuple([s.number(), s.number()]); // Schema<[number, number]>
|
|
1925
|
+
*
|
|
1926
|
+
* @template {[Schema<any>, ...Schema<any>[]] | []} T
|
|
1927
|
+
* @param {T} items
|
|
1928
|
+
* @returns {TupleSchema<{ -readonly [K in keyof T]: Infer<T[K]> }>}
|
|
1929
|
+
*/
|
|
1930
|
+
function tuple(items) {
|
|
1931
|
+
return /** @type {any} */ (new TupleSchema(items));
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* An object with any keys whose values match a schema, like a dictionary.
|
|
1936
|
+
*
|
|
1937
|
+
* @example
|
|
1938
|
+
* s.record(s.number()); // Record<string, number>
|
|
1939
|
+
* s.record(s.boolean(), s.string().regex(/^[a-z]+$/)); // keys checked too
|
|
1940
|
+
*
|
|
1941
|
+
* @template V
|
|
1942
|
+
* @param {Schema<V>} values
|
|
1943
|
+
* @param {Schema<string>} [keys]
|
|
1944
|
+
* @returns {RecordSchema<V>}
|
|
1945
|
+
*/
|
|
1946
|
+
function record(values, keys) {
|
|
1947
|
+
return new RecordSchema(values, keys);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* A schema defined later, for recursive data like trees.
|
|
1952
|
+
*
|
|
1953
|
+
* @example
|
|
1954
|
+
* const Category = s.object({
|
|
1955
|
+
* name: s.string(),
|
|
1956
|
+
* children: s.lazy(() => s.array(Category)).default([]),
|
|
1957
|
+
* });
|
|
1958
|
+
*
|
|
1959
|
+
* @template T
|
|
1960
|
+
* @param {() => Schema<T>} getter
|
|
1961
|
+
* @returns {LazySchema<T>}
|
|
1962
|
+
*/
|
|
1963
|
+
function lazy(getter) {
|
|
1964
|
+
return new LazySchema(getter);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* An instance of a class.
|
|
1969
|
+
*
|
|
1970
|
+
* @example
|
|
1971
|
+
* s.instanceOf(URL);
|
|
1972
|
+
*
|
|
1973
|
+
* @template {new (...args: any[]) => any} C
|
|
1974
|
+
* @param {C} ctor
|
|
1975
|
+
* @param {Message} [message]
|
|
1976
|
+
* @returns {CustomSchema<InstanceType<C>>}
|
|
1977
|
+
*/
|
|
1978
|
+
function instanceOf(ctor, message = `Expected an instance of ${ctor.name}`) {
|
|
1979
|
+
return new CustomSchema((value) => value instanceof ctor, message, ctor.name);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* A schema from any test, usually a type guard.
|
|
1984
|
+
*
|
|
1985
|
+
* @example
|
|
1986
|
+
* const Color = s.custom(nc.isHexColor, "Expected a hex color");
|
|
1987
|
+
*
|
|
1988
|
+
* @template [T=unknown]
|
|
1989
|
+
* @param {((value: unknown) => value is T) | ((value: unknown) => boolean)} test
|
|
1990
|
+
* @param {Message} [message]
|
|
1991
|
+
* @returns {CustomSchema<T>}
|
|
1992
|
+
*/
|
|
1993
|
+
function custom(test, message = "Invalid value") {
|
|
1994
|
+
return new CustomSchema(test, message, "custom");
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* Anything, typed as `any`.
|
|
1999
|
+
* @returns {Schema<any>}
|
|
2000
|
+
*/
|
|
2001
|
+
function any() {
|
|
2002
|
+
return new Schema();
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Anything, typed as `unknown`.
|
|
2007
|
+
* @returns {Schema<unknown>}
|
|
2008
|
+
*/
|
|
2009
|
+
function unknown() {
|
|
2010
|
+
return new Schema();
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* The `s.coerce` builders.
|
|
2015
|
+
* @typedef {object} CoerceBuilders
|
|
2016
|
+
* @property {() => StringSchema} string A string schema that turns any value into a string first.
|
|
2017
|
+
* @property {() => NumberSchema} number A number schema that converts strings like `"42"`, booleans and dates first.
|
|
2018
|
+
* @property {() => BooleanSchema} boolean A boolean schema that understands `"true"`, `"1"`, `"yes"`, `"on"` and their opposites.
|
|
2019
|
+
* @property {() => DateSchema} date A date schema that converts ISO strings and timestamps.
|
|
2020
|
+
* @property {() => BigIntSchema} bigint A bigint schema that converts numeric strings and integers.
|
|
2021
|
+
*/
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* Schemas that convert their input before checking it. Query strings, form
|
|
2025
|
+
* fields and environment variables are always strings, so this is what you
|
|
2026
|
+
* want for them.
|
|
2027
|
+
*
|
|
2028
|
+
* @example
|
|
2029
|
+
* const Query = s.object({
|
|
2030
|
+
* page: s.coerce.number().int().min(1).default(1),
|
|
2031
|
+
* active: s.coerce.boolean().default(false),
|
|
2032
|
+
* });
|
|
2033
|
+
* Query.parse({ page: "2", active: "true" }); // { page: 2, active: true }
|
|
2034
|
+
*
|
|
2035
|
+
* @type {CoerceBuilders}
|
|
2036
|
+
*/
|
|
2037
|
+
const coerce = {
|
|
2038
|
+
/**
|
|
2039
|
+
* @returns {StringSchema}
|
|
2040
|
+
*/
|
|
2041
|
+
string() {
|
|
2042
|
+
const schema = new StringSchema();
|
|
2043
|
+
/** @type {any} */ (schema)._coerce = true;
|
|
2044
|
+
return schema;
|
|
2045
|
+
},
|
|
2046
|
+
/**
|
|
2047
|
+
* @returns {NumberSchema}
|
|
2048
|
+
*/
|
|
2049
|
+
number() {
|
|
2050
|
+
const schema = new NumberSchema();
|
|
2051
|
+
/** @type {any} */ (schema)._coerce = true;
|
|
2052
|
+
return schema;
|
|
2053
|
+
},
|
|
2054
|
+
/**
|
|
2055
|
+
* @returns {BooleanSchema}
|
|
2056
|
+
*/
|
|
2057
|
+
boolean() {
|
|
2058
|
+
const schema = new BooleanSchema();
|
|
2059
|
+
/** @type {any} */ (schema)._coerce = true;
|
|
2060
|
+
return schema;
|
|
2061
|
+
},
|
|
2062
|
+
/**
|
|
2063
|
+
* @returns {DateSchema}
|
|
2064
|
+
*/
|
|
2065
|
+
date() {
|
|
2066
|
+
const schema = new DateSchema();
|
|
2067
|
+
/** @type {any} */ (schema)._coerce = true;
|
|
2068
|
+
return schema;
|
|
2069
|
+
},
|
|
2070
|
+
/**
|
|
2071
|
+
* @returns {BigIntSchema}
|
|
2072
|
+
*/
|
|
2073
|
+
bigint() {
|
|
2074
|
+
const schema = new BigIntSchema();
|
|
2075
|
+
/** @type {any} */ (schema)._coerce = true;
|
|
2076
|
+
return schema;
|
|
2077
|
+
},
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
module.exports = {
|
|
2081
|
+
string,
|
|
2082
|
+
number,
|
|
2083
|
+
boolean,
|
|
2084
|
+
bigint,
|
|
2085
|
+
date,
|
|
2086
|
+
literal,
|
|
2087
|
+
enum: enumeration,
|
|
2088
|
+
array,
|
|
2089
|
+
object,
|
|
2090
|
+
union,
|
|
2091
|
+
discriminatedUnion,
|
|
2092
|
+
tuple,
|
|
2093
|
+
record,
|
|
2094
|
+
lazy,
|
|
2095
|
+
instanceOf,
|
|
2096
|
+
custom,
|
|
2097
|
+
any,
|
|
2098
|
+
unknown,
|
|
2099
|
+
coerce,
|
|
2100
|
+
Schema,
|
|
2101
|
+
};
|