ascertain 2.1.0 → 3.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/README.md +252 -114
- package/build/index.cjs +441 -184
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +168 -186
- package/build/index.js +435 -184
- package/build/index.js.map +1 -1
- package/package.json +13 -12
- package/src/index.ts +605 -380
package/src/index.ts
CHANGED
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstract base class for schema operators.
|
|
3
|
-
*
|
|
4
|
-
* Provides a common constructor that enforces having at least one schema.
|
|
5
|
-
*
|
|
6
|
-
* @template T - The type of data the operator validates.
|
|
7
|
-
* @abstract
|
|
8
|
-
* @internal
|
|
9
|
-
*/
|
|
10
|
-
abstract class Operator<T> {
|
|
11
|
-
constructor(public readonly schemas: Schema<T>[]) {
|
|
12
|
-
if (schemas.length === 0) {
|
|
13
|
-
throw new TypeError(`Operation schema ${this.constructor.name} must have at least one element`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// https://standardschema.dev/
|
|
19
|
-
|
|
20
1
|
/**
|
|
21
2
|
* Symbol for validating object keys against a schema.
|
|
22
3
|
*/
|
|
@@ -30,6 +11,66 @@ export const $values = Symbol.for('@@values');
|
|
|
30
11
|
*/
|
|
31
12
|
export const $strict = Symbol.for('@@strict');
|
|
32
13
|
|
|
14
|
+
const $op = Symbol.for('@@op');
|
|
15
|
+
|
|
16
|
+
const OR = Symbol.for('@@or');
|
|
17
|
+
const AND = Symbol.for('@@and');
|
|
18
|
+
const OPTIONAL = Symbol.for('@@optional');
|
|
19
|
+
const TUPLE = Symbol.for('@@tuple');
|
|
20
|
+
const DISCRIMINATED = Symbol.for('@@discriminated');
|
|
21
|
+
|
|
22
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
23
|
+
|
|
24
|
+
interface OrShape<T> {
|
|
25
|
+
readonly schemas: Schema<T>[];
|
|
26
|
+
readonly [$op]: typeof OR;
|
|
27
|
+
}
|
|
28
|
+
interface AndShape<T> {
|
|
29
|
+
readonly schemas: Schema<T>[];
|
|
30
|
+
readonly [$op]: typeof AND;
|
|
31
|
+
}
|
|
32
|
+
interface OptionalShape<T> {
|
|
33
|
+
readonly schemas: Schema<T>[];
|
|
34
|
+
readonly [$op]: typeof OPTIONAL;
|
|
35
|
+
}
|
|
36
|
+
interface TupleShape<T> {
|
|
37
|
+
readonly schemas: Schema<T>[];
|
|
38
|
+
readonly [$op]: typeof TUPLE;
|
|
39
|
+
}
|
|
40
|
+
interface DiscriminatedShape<T> {
|
|
41
|
+
readonly schemas: Schema<T>[];
|
|
42
|
+
readonly [$op]: typeof DISCRIMINATED;
|
|
43
|
+
readonly key: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Tagged<T> = OrShape<T> | AndShape<T> | OptionalShape<T> | TupleShape<T> | DiscriminatedShape<T>;
|
|
47
|
+
|
|
48
|
+
const OrCtor = function <T>(this: OrShape<T>, schemas: Schema<T>[]) {
|
|
49
|
+
(this as Mutable<OrShape<T>>).schemas = schemas;
|
|
50
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): OrShape<T>; prototype: { [$op]: typeof OR } };
|
|
51
|
+
OrCtor.prototype[$op] = OR;
|
|
52
|
+
|
|
53
|
+
const AndCtor = function <T>(this: AndShape<T>, schemas: Schema<T>[]) {
|
|
54
|
+
(this as Mutable<AndShape<T>>).schemas = schemas;
|
|
55
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): AndShape<T>; prototype: { [$op]: typeof AND } };
|
|
56
|
+
AndCtor.prototype[$op] = AND;
|
|
57
|
+
|
|
58
|
+
const OptionalCtor = function <T>(this: OptionalShape<T>, schema: Schema<T>) {
|
|
59
|
+
(this as Mutable<OptionalShape<T>>).schemas = [schema];
|
|
60
|
+
} as unknown as { new <T>(schema: Schema<T>): OptionalShape<T>; prototype: { [$op]: typeof OPTIONAL } };
|
|
61
|
+
OptionalCtor.prototype[$op] = OPTIONAL;
|
|
62
|
+
|
|
63
|
+
const TupleCtor = function <T>(this: TupleShape<T>, schemas: Schema<T>[]) {
|
|
64
|
+
(this as Mutable<TupleShape<T>>).schemas = schemas;
|
|
65
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): TupleShape<T>; prototype: { [$op]: typeof TUPLE } };
|
|
66
|
+
TupleCtor.prototype[$op] = TUPLE;
|
|
67
|
+
|
|
68
|
+
const DiscriminatedCtor = function <T>(this: DiscriminatedShape<T>, schemas: Schema<T>[], key: string) {
|
|
69
|
+
(this as Mutable<DiscriminatedShape<T>>).schemas = schemas;
|
|
70
|
+
(this as Mutable<DiscriminatedShape<T>>).key = key;
|
|
71
|
+
} as unknown as { new <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T>; prototype: { [$op]: typeof DISCRIMINATED } };
|
|
72
|
+
DiscriminatedCtor.prototype[$op] = DISCRIMINATED;
|
|
73
|
+
|
|
33
74
|
/**
|
|
34
75
|
* Represents a schema for validating data.
|
|
35
76
|
*
|
|
@@ -44,167 +85,58 @@ export type Schema<T> =
|
|
|
44
85
|
? Schema<A>[] | unknown
|
|
45
86
|
: unknown;
|
|
46
87
|
|
|
47
|
-
class Or<T> extends Operator<T> {}
|
|
48
88
|
/**
|
|
49
89
|
* Operator for validating data against any of the provided schemas (logical OR).
|
|
50
|
-
*
|
|
51
|
-
* Creates a schema that accepts data matching any one of the provided schemas.
|
|
52
|
-
* This is useful for creating union types or alternative validation paths.
|
|
53
|
-
*
|
|
54
|
-
* @template T - The type of data the operator validates.
|
|
55
|
-
* @param schemas - Multiple schemas where at least one must match the data.
|
|
56
|
-
* @returns A schema that validates data against any of the provided schemas.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```typescript
|
|
60
|
-
* import { or, ascertain } from 'ascertain';
|
|
61
|
-
*
|
|
62
|
-
* // Create a schema that accepts either a string or number
|
|
63
|
-
* const stringOrNumber = or(String, Number);
|
|
64
|
-
*
|
|
65
|
-
* ascertain(stringOrNumber, "hello", "value"); // ✓ Valid
|
|
66
|
-
* ascertain(stringOrNumber, 42, "value"); // ✓ Valid
|
|
67
|
-
* ascertain(stringOrNumber, true, "value"); // ✗ Throws error
|
|
68
|
-
*
|
|
69
|
-
* // Union of literal values
|
|
70
|
-
* const statusSchema = or('pending', 'completed', 'failed');
|
|
71
|
-
* ascertain(statusSchema, 'pending', "status"); // ✓ Valid
|
|
72
|
-
*
|
|
73
|
-
* // Complex schema combinations
|
|
74
|
-
* const userIdSchema = or(Number, { id: Number, temp: Boolean });
|
|
75
|
-
* ascertain(userIdSchema, 123, "userId"); // ✓ Valid
|
|
76
|
-
* ascertain(userIdSchema, { id: 456, temp: true }, "userId"); // ✓ Valid
|
|
77
|
-
* ```
|
|
78
90
|
*/
|
|
79
|
-
export const or = <T>(...schemas: Schema<T>[]) =>
|
|
91
|
+
export const or = <T>(...schemas: Schema<T>[]): OrShape<T> => {
|
|
92
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
93
|
+
return new OrCtor(schemas);
|
|
94
|
+
};
|
|
80
95
|
|
|
81
|
-
class And<T> extends Operator<T> {}
|
|
82
96
|
/**
|
|
83
97
|
* Operator for validating data against all provided schemas (logical AND).
|
|
84
|
-
*
|
|
85
|
-
* Creates a schema that requires data to match every one of the provided schemas.
|
|
86
|
-
* This is useful for combining multiple validation requirements or adding constraints.
|
|
87
|
-
*
|
|
88
|
-
* @template T - The type of data the operator validates.
|
|
89
|
-
* @param schemas - Multiple schemas that all must match the data.
|
|
90
|
-
* @returns A schema that validates data against all of the provided schemas.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```typescript
|
|
94
|
-
* import { and, ascertain } from 'ascertain';
|
|
95
|
-
*
|
|
96
|
-
* // Combine object schema with additional constraints
|
|
97
|
-
* const userSchema = and(
|
|
98
|
-
* { name: String, age: Number },
|
|
99
|
-
* { age: Number } // Additional constraint
|
|
100
|
-
* );
|
|
101
|
-
*
|
|
102
|
-
* ascertain(userSchema, { name: "John", age: 25 }, "user"); // ✓ Valid
|
|
103
|
-
*
|
|
104
|
-
* // Ensure an object is both a Date and has specific methods
|
|
105
|
-
* const validDateSchema = and(Date, { toISOString: Function });
|
|
106
|
-
* ascertain(validDateSchema, new Date(), "date"); // ✓ Valid
|
|
107
|
-
*
|
|
108
|
-
* ```
|
|
109
98
|
*/
|
|
110
|
-
export const and = <T>(...schemas: Schema<T>[]) =>
|
|
99
|
+
export const and = <T>(...schemas: Schema<T>[]): AndShape<T> => {
|
|
100
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
101
|
+
return new AndCtor(schemas);
|
|
102
|
+
};
|
|
111
103
|
|
|
112
|
-
class Optional<T> extends Operator<T> {
|
|
113
|
-
constructor(schema: Schema<T>) {
|
|
114
|
-
super([schema]);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
104
|
/**
|
|
118
105
|
* Operator for making a schema optional (nullable).
|
|
119
|
-
*
|
|
120
|
-
* Creates a schema that accepts the provided schema or null/undefined values.
|
|
121
|
-
* This is useful for optional object properties or nullable fields.
|
|
122
|
-
*
|
|
123
|
-
* @template T - The type of data the operator validates.
|
|
124
|
-
* @param schema - The schema to make optional.
|
|
125
|
-
* @returns A schema that validates data against the provided schema or accepts null/undefined.
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```typescript
|
|
129
|
-
* import { optional, ascertain } from 'ascertain';
|
|
130
|
-
*
|
|
131
|
-
* // Optional string field
|
|
132
|
-
* const userSchema = {
|
|
133
|
-
* name: String,
|
|
134
|
-
* nickname: optional(String),
|
|
135
|
-
* age: Number
|
|
136
|
-
* };
|
|
137
|
-
*
|
|
138
|
-
* // All of these are valid
|
|
139
|
-
* ascertain(userSchema, {
|
|
140
|
-
* name: "John",
|
|
141
|
-
* nickname: "Johnny",
|
|
142
|
-
* age: 30
|
|
143
|
-
* }, "user"); // ✓ Valid
|
|
144
|
-
*
|
|
145
|
-
* ascertain(userSchema, {
|
|
146
|
-
* name: "Jane",
|
|
147
|
-
* nickname: null,
|
|
148
|
-
* age: 25
|
|
149
|
-
* }, "user"); // ✓ Valid
|
|
150
|
-
*
|
|
151
|
-
* ascertain(userSchema, {
|
|
152
|
-
* name: "Bob",
|
|
153
|
-
* age: 35
|
|
154
|
-
* // nickname is undefined
|
|
155
|
-
* }, "user"); // ✓ Valid
|
|
156
|
-
*
|
|
157
|
-
* // Optional complex objects
|
|
158
|
-
* const profileSchema = {
|
|
159
|
-
* id: Number,
|
|
160
|
-
* settings: optional({
|
|
161
|
-
* theme: String,
|
|
162
|
-
* notifications: Boolean
|
|
163
|
-
* })
|
|
164
|
-
* };
|
|
165
|
-
* ```
|
|
166
106
|
*/
|
|
167
|
-
export const optional = <T>(schema: Schema<T>) => new
|
|
107
|
+
export const optional = <T>(schema: Schema<T>): OptionalShape<T> => new OptionalCtor(schema);
|
|
168
108
|
|
|
169
|
-
class Tuple<T> extends Operator<T> {}
|
|
170
109
|
/**
|
|
171
110
|
* Operator for validating data against a fixed-length tuple of schemas.
|
|
111
|
+
*/
|
|
112
|
+
export const tuple = <T>(...schemas: Schema<T>[]): TupleShape<T> => {
|
|
113
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
114
|
+
return new TupleCtor(schemas);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Operator for validating data against a discriminated union.
|
|
172
119
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
120
|
+
* Optimizes validation by checking the discriminant field first and only
|
|
121
|
+
* validating the matching variant. More efficient than `or()` for unions
|
|
122
|
+
* where each variant has a common field with a unique literal value.
|
|
175
123
|
*
|
|
176
|
-
* @
|
|
177
|
-
* @param
|
|
178
|
-
* @returns A schema that validates data as a tuple with the specified structure.
|
|
124
|
+
* @param schemas - Array of object schemas, each with a discriminant field containing a literal value.
|
|
125
|
+
* @param key - The name of the discriminant field present in all variants.
|
|
179
126
|
*
|
|
180
127
|
* @example
|
|
181
128
|
* ```typescript
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* ascertain(pointSchema, [1.5, 2.7], "point"); // ✓ Valid
|
|
188
|
-
* ascertain(pointSchema, [10], "point"); // ✗ Throws error (too short)
|
|
189
|
-
* ascertain(pointSchema, [10, 20, 30], "point"); // ✗ Throws error (too long)
|
|
190
|
-
*
|
|
191
|
-
* // RGB color tuple
|
|
192
|
-
* const colorSchema = tuple(Number, Number, Number);
|
|
193
|
-
* ascertain(colorSchema, [255, 128, 0], "color"); // ✓ Valid
|
|
194
|
-
*
|
|
195
|
-
* // Mixed type tuple
|
|
196
|
-
* const userInfoSchema = tuple(String, Number, Boolean);
|
|
197
|
-
* ascertain(userInfoSchema, ["Alice", 25, true], "userInfo"); // ✓ Valid
|
|
198
|
-
*
|
|
199
|
-
* // Nested tuple
|
|
200
|
-
* const lineSchema = tuple(
|
|
201
|
-
* tuple(Number, Number), // start point
|
|
202
|
-
* tuple(Number, Number) // end point
|
|
203
|
-
* );
|
|
204
|
-
* ascertain(lineSchema, [[0, 0], [10, 10]], "line"); // ✓ Valid
|
|
129
|
+
* const messageSchema = discriminated([
|
|
130
|
+
* { type: 'email', address: String },
|
|
131
|
+
* { type: 'sms', phone: String },
|
|
132
|
+
* { type: 'push', token: String },
|
|
133
|
+
* ], 'type');
|
|
205
134
|
* ```
|
|
206
135
|
*/
|
|
207
|
-
export const
|
|
136
|
+
export const discriminated = <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T> => {
|
|
137
|
+
if (schemas.length === 0) throw new TypeError('discriminated requires at least one schema');
|
|
138
|
+
return new DiscriminatedCtor(schemas, key);
|
|
139
|
+
};
|
|
208
140
|
|
|
209
141
|
/**
|
|
210
142
|
* Decodes a base64-encoded string to UTF-8.
|
|
@@ -214,7 +146,9 @@ export const tuple = <T>(...schemas: Schema<T>[]) => new Tuple(schemas);
|
|
|
214
146
|
* @param value - The base64-encoded string to decode.
|
|
215
147
|
* @returns The decoded UTF-8 string.
|
|
216
148
|
*/
|
|
217
|
-
export const fromBase64 =
|
|
149
|
+
export const fromBase64 =
|
|
150
|
+
/* c8 ignore next */
|
|
151
|
+
typeof Buffer === 'undefined' ? (value: string) => atob(value) : (value: string) => Buffer.from(value, 'base64').toString('utf-8');
|
|
218
152
|
|
|
219
153
|
const MULTIPLIERS = {
|
|
220
154
|
ms: 1,
|
|
@@ -277,7 +211,7 @@ export const as = {
|
|
|
277
211
|
return value[0] === '-' ? -result : result;
|
|
278
212
|
}
|
|
279
213
|
|
|
280
|
-
const result = value.
|
|
214
|
+
const result = value.trim() ? Number(value) : NaN;
|
|
281
215
|
return Number.isNaN(result) ? asError(`Invalid value ${value}, expected a valid number`) : result;
|
|
282
216
|
},
|
|
283
217
|
/**
|
|
@@ -389,232 +323,387 @@ class Context {
|
|
|
389
323
|
}
|
|
390
324
|
}
|
|
391
325
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const ${valueAlias} = ${valuePath}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
326
|
+
type Mode =
|
|
327
|
+
| { fast: true; onFail?: string }
|
|
328
|
+
| { fast: false; firstError: true; issues: string; path: PropertyKey[]; pathExpr: string }
|
|
329
|
+
| { fast: false; firstError: false; issues: string; path: PropertyKey[]; pathExpr: string };
|
|
330
|
+
|
|
331
|
+
const isTagged = (schema: unknown): schema is Tagged<unknown> => (schema as Tagged<unknown>)?.[$op] !== undefined;
|
|
332
|
+
|
|
333
|
+
const childMode = (mode: Exclude<Mode, { fast: true }>, key: PropertyKey | { dynamic: string }): Mode => {
|
|
334
|
+
if (typeof key === 'object' && 'dynamic' in key) {
|
|
335
|
+
return {
|
|
336
|
+
fast: false,
|
|
337
|
+
firstError: mode.firstError,
|
|
338
|
+
issues: mode.issues,
|
|
339
|
+
path: mode.path,
|
|
340
|
+
pathExpr: `[${mode.path.map((k) => JSON.stringify(k)).join(',')}${mode.path.length ? ',' : ''}${key.dynamic}]`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const newPath = [...mode.path, key];
|
|
344
|
+
return { fast: false, firstError: mode.firstError, issues: mode.issues, path: newPath, pathExpr: JSON.stringify(newPath) };
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const toLiteral = (value: unknown): string => (typeof value === 'bigint' ? `${value}n` : JSON.stringify(value));
|
|
348
|
+
|
|
349
|
+
const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode: Mode): string => {
|
|
350
|
+
const emit = mode.fast
|
|
351
|
+
? null
|
|
352
|
+
: mode.firstError
|
|
353
|
+
? (msg: string) => `${mode.issues} = [{ message: ${msg}, path: ${mode.pathExpr} }]; return ${mode.issues};`
|
|
354
|
+
: (msg: string) => `(${mode.issues} || (${mode.issues} = [])).push({ message: ${msg}, path: ${mode.pathExpr} });`;
|
|
355
|
+
const fail = mode.fast ? (mode.onFail ?? 'return false;') : '';
|
|
356
|
+
|
|
357
|
+
if (isTagged(schema)) {
|
|
358
|
+
const tag = schema[$op];
|
|
359
|
+
if (tag === AND) {
|
|
360
|
+
const valueAlias = context.unique('v');
|
|
361
|
+
const code = schema.schemas.map((s) => codeGen(s, context, valueAlias, mode)).join('\n');
|
|
362
|
+
return `const ${valueAlias} = ${valuePath};\n${code}`;
|
|
363
|
+
} else if (tag === OR) {
|
|
364
|
+
const valueAlias = context.unique('v');
|
|
365
|
+
const foundValid = context.unique('valid');
|
|
366
|
+
if (mode.fast) {
|
|
367
|
+
const branches = schema.schemas.map((s) => {
|
|
368
|
+
const branchValid = context.unique('valid');
|
|
369
|
+
const branchCode = codeGen(s, context, valueAlias, { ...mode, onFail: `${branchValid} = false;` });
|
|
370
|
+
return `if (!${foundValid}) { let ${branchValid} = true; ${branchCode} if (${branchValid}) { ${foundValid} = true; } }`;
|
|
371
|
+
});
|
|
372
|
+
return `const ${valueAlias} = ${valuePath};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { ${fail} }`;
|
|
373
|
+
} else if (mode.firstError) {
|
|
374
|
+
const firstBranchIssues = context.unique('iss');
|
|
375
|
+
const branches = schema.schemas.map((s, idx) => {
|
|
376
|
+
const branchIssues = context.unique('iss');
|
|
377
|
+
const branchCode = codeGen(s, context, valueAlias, {
|
|
378
|
+
fast: false,
|
|
379
|
+
firstError: true,
|
|
380
|
+
issues: branchIssues,
|
|
381
|
+
path: mode.path,
|
|
382
|
+
pathExpr: mode.pathExpr,
|
|
383
|
+
}).replace(new RegExp(`; return ${branchIssues};`, 'g'), ';');
|
|
384
|
+
if (idx === 0) {
|
|
385
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${firstBranchIssues} = ${branchIssues}; } }`;
|
|
386
|
+
}
|
|
387
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } }`;
|
|
388
|
+
});
|
|
389
|
+
return `const ${valueAlias} = ${valuePath};\nlet ${firstBranchIssues};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { return ${firstBranchIssues}; }`;
|
|
390
|
+
} else {
|
|
391
|
+
const localIssues = context.unique('iss');
|
|
392
|
+
const branches = schema.schemas.map((s) => {
|
|
393
|
+
const branchIssues = context.unique('iss');
|
|
394
|
+
const branchCode = codeGen(s, context, valueAlias, {
|
|
395
|
+
fast: false,
|
|
396
|
+
firstError: false,
|
|
397
|
+
issues: branchIssues,
|
|
398
|
+
path: mode.path,
|
|
399
|
+
pathExpr: mode.pathExpr,
|
|
400
|
+
});
|
|
401
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${localIssues}.push(...${branchIssues}); } }`;
|
|
402
|
+
});
|
|
403
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${localIssues} = [];\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { (${mode.issues} || (${mode.issues} = [])).push(...${localIssues}); }`;
|
|
404
|
+
}
|
|
405
|
+
} else if (tag === OPTIONAL) {
|
|
406
|
+
const valueAlias = context.unique('v');
|
|
407
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen((schema as OptionalShape<T>).schemas[0], context, valueAlias, mode)} }`;
|
|
408
|
+
} else if (tag === TUPLE) {
|
|
409
|
+
const valueAlias = context.unique('v');
|
|
410
|
+
if (mode.fast) {
|
|
411
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object' || !Array.isArray(${valueAlias}) || ${valueAlias}.length !== ${schema.schemas.length}) { ${fail} }\n${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, mode)).join('\n')}`;
|
|
412
|
+
} else {
|
|
413
|
+
return [
|
|
414
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
415
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
416
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
|
|
417
|
+
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
418
|
+
`else if (${valueAlias}.length !== ${schema.schemas.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.schemas.length}\``)} }`,
|
|
419
|
+
`else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`,
|
|
420
|
+
].join('\n');
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const { key, schemas } = schema as DiscriminatedShape<T>;
|
|
424
|
+
const valueAlias = context.unique('v');
|
|
425
|
+
const discriminantAlias = context.unique('d');
|
|
426
|
+
const keyStr = JSON.stringify(key);
|
|
427
|
+
|
|
428
|
+
const variants: { value: unknown; schema: Schema<T> }[] = [];
|
|
429
|
+
for (const s of schemas) {
|
|
430
|
+
if (typeof s !== 'object' || s === null || !(key in s)) {
|
|
431
|
+
throw new TypeError(`discriminated: each schema must have the discriminant key "${key}"`);
|
|
432
|
+
}
|
|
433
|
+
const discriminantValue = (s as Record<string, unknown>)[key];
|
|
434
|
+
if (typeof discriminantValue !== 'string' && typeof discriminantValue !== 'number' && typeof discriminantValue !== 'boolean') {
|
|
435
|
+
throw new TypeError(`discriminated: discriminant value must be a string, number, or boolean literal`);
|
|
436
|
+
}
|
|
437
|
+
variants.push({ value: discriminantValue, schema: s });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (mode.fast) {
|
|
441
|
+
const branches = variants.map(({ value, schema: s }) => {
|
|
442
|
+
const branchCode = codeGen(s, context, valueAlias, mode);
|
|
443
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
444
|
+
});
|
|
445
|
+
return [
|
|
446
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
447
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`,
|
|
448
|
+
`const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
449
|
+
branches.join(' else ') + ` else { ${fail} }`,
|
|
450
|
+
].join('\n');
|
|
451
|
+
} else {
|
|
452
|
+
const validValues = variants.map((v) => JSON.stringify(v.value)).join(', ');
|
|
453
|
+
const branches = variants.map(({ value, schema: s }) => {
|
|
454
|
+
const branchCode = codeGen(s, context, valueAlias, mode);
|
|
455
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
456
|
+
});
|
|
457
|
+
return [
|
|
458
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
459
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
460
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an object\``)} }`,
|
|
461
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
462
|
+
`else {`,
|
|
463
|
+
` const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
464
|
+
` ${branches.join(' else ')} else { ${emit!(`\`Invalid discriminant value \${JSON.stringify(${discriminantAlias})}, expected one of: ${validValues}\``)} }`,
|
|
465
|
+
`}`,
|
|
466
|
+
].join('\n');
|
|
467
|
+
}
|
|
451
468
|
}
|
|
469
|
+
}
|
|
452
470
|
|
|
471
|
+
if (typeof schema === 'function') {
|
|
472
|
+
const valueAlias = context.unique('v');
|
|
453
473
|
const name = (schema as { name?: string })?.name;
|
|
474
|
+
const s = schema as unknown;
|
|
454
475
|
const primitiveType =
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`if (typeof ${valueAlias} !== '${primitiveType}') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected type ${schema?.name}\`); }`,
|
|
470
|
-
);
|
|
471
|
-
if (primitiveType === 'number') {
|
|
472
|
-
code.push(
|
|
473
|
-
`if (Number.isNaN(${valueAlias})) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected a valid ${schema?.name}\`); }`,
|
|
474
|
-
);
|
|
476
|
+
s === String ? 'string' : s === Number ? 'number' : s === Boolean ? 'boolean' : s === BigInt ? 'bigint' : s === Symbol ? 'symbol' : null;
|
|
477
|
+
|
|
478
|
+
if (mode.fast) {
|
|
479
|
+
if (primitiveType) {
|
|
480
|
+
const checks = [`typeof ${valueAlias} !== '${primitiveType}'`];
|
|
481
|
+
if (primitiveType === 'number') checks.push(`Number.isNaN(${valueAlias})`);
|
|
482
|
+
return `const ${valueAlias} = ${valuePath};\nif (${checks.join(' || ')}) { ${fail} }`;
|
|
483
|
+
} else if (name === 'Function') {
|
|
484
|
+
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== 'function') { ${fail} }`;
|
|
485
|
+
} else {
|
|
486
|
+
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
487
|
+
const index = context.register(schema);
|
|
488
|
+
const registryAlias = context.unique('r');
|
|
489
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (${valueAlias} === null || ${valueAlias} === undefined${isError ? '' : ` || ${valueAlias} instanceof Error`} || (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) || (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) || Number.isNaN(${valueAlias}?.valueOf?.())) { ${fail} }`;
|
|
475
490
|
}
|
|
476
|
-
} else if (name === 'Function') {
|
|
477
|
-
code.push(
|
|
478
|
-
`if (typeof ${valueAlias} !== 'function') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected type Function\`); }`,
|
|
479
|
-
);
|
|
480
491
|
} else {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
`
|
|
485
|
-
`if (typeof ${valueAlias}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
];
|
|
500
|
-
if (schema.length > 0) {
|
|
501
|
-
const value = context.unique('val');
|
|
502
|
-
const key = context.unique('key');
|
|
503
|
-
const errorsAlias = context.unique('err');
|
|
504
|
-
code.push(`const ${errorsAlias} = [];`);
|
|
505
|
-
|
|
506
|
-
if (schema.length === 1) {
|
|
492
|
+
const code: string[] = [`const ${valueAlias} = ${valuePath};`];
|
|
493
|
+
if (primitiveType) {
|
|
494
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
495
|
+
code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
496
|
+
code.push(`else if (typeof ${valueAlias} !== '${primitiveType}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type ${name}\``)} }`);
|
|
497
|
+
if (primitiveType === 'number')
|
|
498
|
+
code.push(`else if (Number.isNaN(${valueAlias})) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
|
|
499
|
+
} else if (name === 'Function') {
|
|
500
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
501
|
+
code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
502
|
+
code.push(`else if (typeof ${valueAlias} !== 'function') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type Function\``)} }`);
|
|
503
|
+
} else {
|
|
504
|
+
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
505
|
+
const index = context.register(schema);
|
|
506
|
+
const registryAlias = context.unique('r');
|
|
507
|
+
code.push(`const ${registryAlias} = ctx.registry[${index}];`);
|
|
508
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
509
|
+
if (!isError) code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
507
510
|
code.push(
|
|
508
|
-
|
|
509
|
-
(s) =>
|
|
510
|
-
`for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGenCollectErrors(errorsAlias, codeGen(s, context, value, `${path}[\${${key}}]`))} }`,
|
|
511
|
-
),
|
|
511
|
+
`else if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}?.constructor?.name}, expected an instance of ${name}\``)} }`,
|
|
512
512
|
);
|
|
513
|
-
} else {
|
|
514
513
|
code.push(
|
|
515
|
-
`if (${valueAlias}
|
|
514
|
+
`else if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { ${emit!(`\`Invalid type \${${valueAlias}?.constructor?.name}, expected type ${name}\``)} }`,
|
|
516
515
|
);
|
|
517
|
-
code.push(
|
|
516
|
+
code.push(`else if (Number.isNaN(${valueAlias}?.valueOf?.())) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
|
|
518
517
|
}
|
|
519
|
-
|
|
520
|
-
code.push(codeGenExpectNoErrors(errorsAlias));
|
|
518
|
+
return code.join('\n');
|
|
521
519
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const ${valueAlias} = ${valuePath}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (Array.isArray(schema)) {
|
|
523
|
+
const valueAlias = context.unique('v');
|
|
524
|
+
if (mode.fast) {
|
|
525
|
+
let code = `const ${valueAlias} = ${valuePath};\nif (!Array.isArray(${valueAlias})) { ${fail} }`;
|
|
526
|
+
if (schema.length === 1) {
|
|
527
|
+
const value = context.unique('val');
|
|
528
|
+
const key = context.unique('key');
|
|
529
|
+
code += `\nfor (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, mode)} }`;
|
|
530
|
+
} else if (schema.length > 1) {
|
|
531
|
+
code += `\nif (${valueAlias}.length > ${schema.length}) { ${fail} }`;
|
|
532
|
+
code += '\n' + schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, mode)).join('\n');
|
|
533
|
+
}
|
|
534
|
+
return code;
|
|
532
535
|
} else {
|
|
533
|
-
const valueAlias = context.unique('v');
|
|
534
536
|
const code: string[] = [
|
|
535
537
|
`const ${valueAlias} = ${valuePath};`,
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
539
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
540
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
|
|
541
|
+
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
539
542
|
];
|
|
540
|
-
if (
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const ${
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
543
|
+
if (schema.length > 0) {
|
|
544
|
+
const value = context.unique('val');
|
|
545
|
+
const key = context.unique('key');
|
|
546
|
+
if (schema.length === 1) {
|
|
547
|
+
// Dynamic key - use runtime concat
|
|
548
|
+
code.push(
|
|
549
|
+
`else { for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, childMode(mode, { dynamic: key }))} } }`,
|
|
550
|
+
);
|
|
551
|
+
} else {
|
|
552
|
+
code.push(
|
|
553
|
+
`else if (${valueAlias}.length > ${schema.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.length}\``)} }`,
|
|
554
|
+
);
|
|
555
|
+
code.push(`else { ${schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`);
|
|
556
|
+
}
|
|
550
557
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
`)
|
|
558
|
+
return code.join('\n');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
563
|
+
if (schema instanceof RegExp) {
|
|
564
|
+
const valueAlias = context.unique('v');
|
|
565
|
+
if (mode.fast) {
|
|
566
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined || ${valueAlias} instanceof Error || !${schema.toString()}.test(String(${valueAlias}))) { ${fail} }`;
|
|
567
|
+
} else {
|
|
568
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }\nelse if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }\nelse if (!${schema.toString()}.test(String(${valueAlias}))) { ${emit!(`\`Invalid value \${${valueAlias}}, expected to match ${schema.toString()}\``)} }`;
|
|
562
569
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
+
} else {
|
|
571
|
+
const valueAlias = context.unique('v');
|
|
572
|
+
if (mode.fast) {
|
|
573
|
+
let code = `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`;
|
|
574
|
+
if ($keys in schema) {
|
|
575
|
+
const keysAlias = context.unique('k');
|
|
576
|
+
const kAlias = context.unique('k');
|
|
577
|
+
code += `\nconst ${keysAlias} = Object.keys(${valueAlias});\nfor (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, mode)} }`;
|
|
578
|
+
}
|
|
579
|
+
if ($values in schema) {
|
|
580
|
+
const vAlias = context.unique('val');
|
|
581
|
+
const kAlias = context.unique('k');
|
|
582
|
+
const entriesAlias = context.unique('en');
|
|
583
|
+
code += `\nconst ${entriesAlias} = Object.entries(${valueAlias});\nfor (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, mode)} }`;
|
|
584
|
+
}
|
|
585
|
+
if ($strict in schema && schema[$strict]) {
|
|
586
|
+
const keysAlias = context.unique('k');
|
|
587
|
+
const kAlias = context.unique('k');
|
|
588
|
+
const extraAlias = context.unique('ex');
|
|
589
|
+
code += `\nconst ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});\nconst ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));\nif (${extraAlias}.length !== 0) { ${fail} }`;
|
|
590
|
+
}
|
|
591
|
+
code +=
|
|
592
|
+
'\n' +
|
|
593
|
+
Object.entries(schema)
|
|
594
|
+
.map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, mode))
|
|
595
|
+
.join('\n');
|
|
596
|
+
return code;
|
|
597
|
+
} else {
|
|
598
|
+
const code: string[] = [
|
|
599
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
600
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
601
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} }`,
|
|
602
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
603
|
+
'else {',
|
|
604
|
+
];
|
|
605
|
+
const innerCode: string[] = [];
|
|
606
|
+
if ($keys in schema) {
|
|
607
|
+
const keysAlias = context.unique('k');
|
|
608
|
+
const kAlias = context.unique('k');
|
|
609
|
+
innerCode.push(`const ${keysAlias} = Object.keys(${valueAlias});`);
|
|
610
|
+
// Dynamic key - use runtime concat
|
|
611
|
+
innerCode.push(`for (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, childMode(mode, { dynamic: kAlias }))} }`);
|
|
612
|
+
}
|
|
613
|
+
if ($values in schema) {
|
|
614
|
+
const vAlias = context.unique('val');
|
|
615
|
+
const kAlias = context.unique('k');
|
|
616
|
+
const entriesAlias = context.unique('en');
|
|
617
|
+
innerCode.push(`const ${entriesAlias} = Object.entries(${valueAlias});`);
|
|
618
|
+
// Dynamic key - use runtime concat
|
|
619
|
+
innerCode.push(
|
|
620
|
+
`for (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, childMode(mode, { dynamic: kAlias }))} }`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if ($strict in schema && schema[$strict]) {
|
|
624
|
+
const keysAlias = context.unique('k');
|
|
625
|
+
const kAlias = context.unique('k');
|
|
626
|
+
const extraAlias = context.unique('ex');
|
|
627
|
+
innerCode.push(`const ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});`);
|
|
628
|
+
innerCode.push(`const ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));`);
|
|
629
|
+
innerCode.push(`if (${extraAlias}.length !== 0) { ${emit!(`\`Extra properties: \${${extraAlias}}, are not allowed\``)} }`);
|
|
630
|
+
}
|
|
631
|
+
// Static keys - pre-register paths
|
|
632
|
+
innerCode.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, childMode(mode, key))));
|
|
633
|
+
code.push(innerCode.join('\n'), '}');
|
|
634
|
+
return code.join('\n');
|
|
570
635
|
}
|
|
571
|
-
code.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, `${path}.${key}`)));
|
|
572
|
-
return `${code.join('\n')}`;
|
|
573
636
|
}
|
|
574
|
-
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (typeof schema === 'symbol') {
|
|
575
640
|
const index = context.register(schema);
|
|
576
641
|
const valueAlias = context.unique('v');
|
|
577
642
|
const registryAlias = context.unique('r');
|
|
643
|
+
if (mode.fast) {
|
|
644
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (typeof ${valueAlias} !== 'symbol' || ${valueAlias} !== ${registryAlias}) { ${fail} }`;
|
|
645
|
+
} else {
|
|
646
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (typeof ${valueAlias} !== 'symbol') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected symbol\``)} }\nelse if (${valueAlias} !== ${registryAlias}) { ${emit!(`\`Invalid value \${${valueAlias}.toString()}, expected ${schema.toString()}\``)} }`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
578
649
|
|
|
579
|
-
|
|
580
|
-
const ${valueAlias} = ${valuePath};
|
|
581
|
-
const ${registryAlias} = ctx.registry[${index}];
|
|
582
|
-
if (typeof ${valueAlias} !== 'symbol') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected symbol\`); }
|
|
583
|
-
if (${valueAlias} !== ${registryAlias}) { throw new TypeError(\`Invalid value \${${valueAlias}.toString()} for path "${path}", expected ${schema.toString()}\`); }
|
|
584
|
-
`;
|
|
585
|
-
} else if (schema === null || schema === undefined) {
|
|
650
|
+
if (schema === null || schema === undefined) {
|
|
586
651
|
const valueAlias = context.unique('v');
|
|
587
|
-
|
|
588
|
-
const ${valueAlias} = ${valuePath}
|
|
589
|
-
|
|
590
|
-
|
|
652
|
+
if (mode.fast) {
|
|
653
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${fail} }`;
|
|
654
|
+
} else {
|
|
655
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected nullable\``)} }`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const valueAlias = context.unique('v');
|
|
660
|
+
if (mode.fast) {
|
|
661
|
+
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== '${typeof schema}' || ${valueAlias} !== ${toLiteral(schema)}) { ${fail} }`;
|
|
591
662
|
} else {
|
|
592
|
-
const valueAlias = context.unique('v');
|
|
593
663
|
const value = context.unique('val');
|
|
594
|
-
return `
|
|
595
|
-
const ${valueAlias} = ${valuePath};
|
|
596
|
-
const ${value} = ${JSON.stringify(schema)};
|
|
597
|
-
${codeGenExpectNonError(valueAlias, path)}
|
|
598
|
-
if (typeof ${valueAlias} !== '${typeof schema}') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected ${typeof schema}\`); }
|
|
599
|
-
if (${valueAlias} !== ${value}) { throw new TypeError(\`Invalid value \${JSON.stringify(${valueAlias})} for path "${path}", expected ${JSON.stringify(schema)}\`); }
|
|
600
|
-
`;
|
|
664
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${value} = ${toLiteral(schema)};\nif (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }\nelse if (typeof ${valueAlias} !== '${typeof schema}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected ${typeof schema}\``)} }\nelse if (${valueAlias} !== ${value}) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected ${toLiteral(schema)}\``)} }`;
|
|
601
665
|
}
|
|
602
666
|
};
|
|
603
667
|
|
|
668
|
+
const emptyIssues: StandardSchemaV1.Issue[] = [];
|
|
669
|
+
|
|
604
670
|
/**
|
|
605
|
-
*
|
|
671
|
+
* Validator function returned by compile().
|
|
672
|
+
* Returns true if valid, false if invalid.
|
|
673
|
+
* Access `.issues` property after validation to get error details.
|
|
674
|
+
*/
|
|
675
|
+
export interface Validator<T> {
|
|
676
|
+
(data: T): boolean;
|
|
677
|
+
issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Options for the compile function.
|
|
682
|
+
*/
|
|
683
|
+
export interface CompileOptions {
|
|
684
|
+
/**
|
|
685
|
+
* When true, collects all validation errors instead of stopping at the first.
|
|
686
|
+
* Default is false (first-error mode) for optimal performance.
|
|
687
|
+
*/
|
|
688
|
+
allErrors?: boolean;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Compiles a schema into a high-performance validation function.
|
|
693
|
+
*
|
|
694
|
+
* By default uses first-error mode which stops at the first validation failure
|
|
695
|
+
* and returns immediately. This provides optimal performance for invalid data.
|
|
606
696
|
*
|
|
607
|
-
*
|
|
608
|
-
* that can be used to validate data against the schema.
|
|
697
|
+
* Set `allErrors: true` to collect all validation errors (slower but more informative).
|
|
609
698
|
*
|
|
610
699
|
* @template T - The type of data the schema validates.
|
|
611
700
|
* @param schema - The schema to compile.
|
|
612
|
-
* @param
|
|
613
|
-
* @returns A
|
|
701
|
+
* @param options - Optional configuration (allErrors: boolean).
|
|
702
|
+
* @returns A validator function that returns boolean. Access `.issues` for error details.
|
|
614
703
|
*
|
|
615
704
|
* @example
|
|
616
705
|
* ```typescript
|
|
617
|
-
* import { compile, optional,
|
|
706
|
+
* import { compile, optional, or } from 'ascertain';
|
|
618
707
|
*
|
|
619
708
|
* const userSchema = {
|
|
620
709
|
* name: String,
|
|
@@ -623,32 +712,65 @@ if (${valueAlias} !== ${value}) { throw new TypeError(\`Invalid value \${JSON.st
|
|
|
623
712
|
* role: or('admin', 'user', 'guest')
|
|
624
713
|
* };
|
|
625
714
|
*
|
|
626
|
-
*
|
|
715
|
+
* // First-error mode (default) - fastest for invalid data
|
|
716
|
+
* const validate = compile(userSchema);
|
|
627
717
|
*
|
|
628
|
-
* //
|
|
629
|
-
*
|
|
630
|
-
* name: 'John Doe',
|
|
631
|
-
* age: 30,
|
|
632
|
-
* email: 'john@example.com',
|
|
633
|
-
* role: 'user'
|
|
634
|
-
* });
|
|
718
|
+
* // All-errors mode - collects all validation issues
|
|
719
|
+
* const validateAll = compile(userSchema, { allErrors: true });
|
|
635
720
|
*
|
|
636
|
-
* //
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* console.
|
|
721
|
+
* // Valid data
|
|
722
|
+
* if (validate({ name: 'John', age: 30, role: 'user' })) {
|
|
723
|
+
* console.log('Valid!');
|
|
724
|
+
* }
|
|
725
|
+
*
|
|
726
|
+
* // Invalid data - check .issues for details
|
|
727
|
+
* if (!validate({ name: 123, age: 'thirty' })) {
|
|
728
|
+
* console.log(validate.issues); // Array with first validation issue
|
|
644
729
|
* }
|
|
645
730
|
* ```
|
|
646
731
|
*/
|
|
647
|
-
export const compile = <T>(schema: Schema<T>,
|
|
732
|
+
export const compile = <T>(schema: Schema<T>, options?: CompileOptions): Validator<T> => {
|
|
733
|
+
const allErrors = options?.allErrors ?? false;
|
|
734
|
+
|
|
735
|
+
if (allErrors) {
|
|
736
|
+
const fastContext = new Context();
|
|
737
|
+
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
738
|
+
const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
|
|
739
|
+
|
|
740
|
+
const issueContext = new Context();
|
|
741
|
+
const issueCode = `let issues;\n${codeGen(schema, issueContext, 'data', { fast: false, firstError: false, issues: 'issues', path: [], pathExpr: '[]' })}\nreturn issues || [];`;
|
|
742
|
+
const issueValidator = new Function('ctx', `return (data) => {\n${issueCode}\n};`)(issueContext) as (data: T) => StandardSchemaV1.Issue[];
|
|
743
|
+
|
|
744
|
+
const validator = ((data: T): boolean => {
|
|
745
|
+
if (fastValidator(data)) {
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
validator.issues = issueValidator(data);
|
|
749
|
+
return false;
|
|
750
|
+
}) as Validator<T>;
|
|
751
|
+
validator.issues = emptyIssues;
|
|
752
|
+
return validator;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const fastContext = new Context();
|
|
756
|
+
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
757
|
+
const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
|
|
758
|
+
|
|
648
759
|
const context = new Context();
|
|
649
|
-
const code = codeGen(schema, context, 'data',
|
|
650
|
-
const
|
|
651
|
-
|
|
760
|
+
const code = codeGen(schema, context, 'data', { fast: false, firstError: true, issues: 'issues', path: [], pathExpr: '[]' });
|
|
761
|
+
const firstErrorValidator = new Function('ctx', `return (data) => {\nlet issues;\n${code}\nreturn issues;\n};`)(context) as (
|
|
762
|
+
data: T,
|
|
763
|
+
) => StandardSchemaV1.Issue[] | undefined;
|
|
764
|
+
|
|
765
|
+
const validator = ((data: T): boolean => {
|
|
766
|
+
if (fastValidator(data)) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
validator.issues = firstErrorValidator(data)!;
|
|
770
|
+
return false;
|
|
771
|
+
}) as Validator<T>;
|
|
772
|
+
validator.issues = emptyIssues;
|
|
773
|
+
return validator;
|
|
652
774
|
};
|
|
653
775
|
|
|
654
776
|
/**
|
|
@@ -660,7 +782,6 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
660
782
|
* @template T - The type of data the schema validates.
|
|
661
783
|
* @param schema - The schema to validate against.
|
|
662
784
|
* @param data - The data to validate.
|
|
663
|
-
* @param rootName - A name for the root of the data structure (used in error messages, defaults to '[root]').
|
|
664
785
|
* @throws `{TypeError}` If the data does not conform to the schema.
|
|
665
786
|
*
|
|
666
787
|
* @example
|
|
@@ -682,7 +803,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
682
803
|
* };
|
|
683
804
|
*
|
|
684
805
|
* // Validate data - throws if invalid, otherwise continues silently
|
|
685
|
-
* ascertain(userSchema, userData
|
|
806
|
+
* ascertain(userSchema, userData);
|
|
686
807
|
* console.log('User data is valid!');
|
|
687
808
|
*
|
|
688
809
|
* // Example with invalid data
|
|
@@ -691,7 +812,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
691
812
|
* name: 'Bob',
|
|
692
813
|
* age: 'twenty-five', // Invalid: should be number
|
|
693
814
|
* active: true
|
|
694
|
-
* }
|
|
815
|
+
* });
|
|
695
816
|
* } catch (error) {
|
|
696
817
|
* console.error('Validation failed:', error.message);
|
|
697
818
|
* }
|
|
@@ -703,17 +824,20 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
703
824
|
* const numbersSchema = [Number];
|
|
704
825
|
* const numbers = [1, 2, 3, 4, 5];
|
|
705
826
|
*
|
|
706
|
-
* ascertain(numbersSchema, numbers
|
|
827
|
+
* ascertain(numbersSchema, numbers);
|
|
707
828
|
*
|
|
708
829
|
* // Tuple validation
|
|
709
830
|
* const coordinateSchema = tuple(Number, Number);
|
|
710
831
|
* const point = [10, 20];
|
|
711
832
|
*
|
|
712
|
-
* ascertain(coordinateSchema, point
|
|
833
|
+
* ascertain(coordinateSchema, point);
|
|
713
834
|
* ```
|
|
714
835
|
*/
|
|
715
|
-
export const ascertain = <T>(schema: Schema<T>, data: T
|
|
716
|
-
compile(schema
|
|
836
|
+
export const ascertain = <T>(schema: Schema<T>, data: T) => {
|
|
837
|
+
const validator = compile(schema);
|
|
838
|
+
if (!validator(data)) {
|
|
839
|
+
throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
|
|
840
|
+
}
|
|
717
841
|
};
|
|
718
842
|
|
|
719
843
|
/**
|
|
@@ -732,7 +856,6 @@ export type ExtractShape<C, S> = {
|
|
|
732
856
|
*
|
|
733
857
|
* @template C - The type of the config object.
|
|
734
858
|
* @param config - The config object to validate against.
|
|
735
|
-
* @param rootName - A name for the root of the data structure (used in error messages).
|
|
736
859
|
* @returns A validator function that takes a schema and returns the typed config subset.
|
|
737
860
|
*
|
|
738
861
|
* @example
|
|
@@ -745,7 +868,7 @@ export type ExtractShape<C, S> = {
|
|
|
745
868
|
* redis: { host: as.string(process.env.REDIS_HOST) },
|
|
746
869
|
* };
|
|
747
870
|
*
|
|
748
|
-
* const validate = createValidator(config
|
|
871
|
+
* const validate = createValidator(config);
|
|
749
872
|
*
|
|
750
873
|
* // Consumer only validates what it needs
|
|
751
874
|
* const { app, kafka } = validate({
|
|
@@ -758,9 +881,111 @@ export type ExtractShape<C, S> = {
|
|
|
758
881
|
* // redis is not accessible - TypeScript error
|
|
759
882
|
* ```
|
|
760
883
|
*/
|
|
761
|
-
export const createValidator = <C>(config: C
|
|
884
|
+
export const createValidator = <C>(config: C) => {
|
|
762
885
|
return <S extends Schema<Partial<C>>>(schema: S): ExtractShape<C, S> => {
|
|
763
|
-
ascertain(schema as Schema<C>, config
|
|
886
|
+
ascertain(schema as Schema<C>, config);
|
|
764
887
|
return config as ExtractShape<C, S>;
|
|
765
888
|
};
|
|
766
889
|
};
|
|
890
|
+
|
|
891
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
892
|
+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export namespace StandardSchemaV1 {
|
|
896
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
897
|
+
readonly version: 1;
|
|
898
|
+
readonly vendor: string;
|
|
899
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
900
|
+
readonly types?: { readonly input: Input; readonly output: Output };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
904
|
+
|
|
905
|
+
export interface SuccessResult<Output> {
|
|
906
|
+
readonly value: Output;
|
|
907
|
+
readonly issues?: undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export interface FailureResult {
|
|
911
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export interface Issue {
|
|
915
|
+
readonly message: string;
|
|
916
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export interface PathSegment {
|
|
920
|
+
readonly key: PropertyKey;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Wraps an Ascertain schema to make it Standard Schema v1 compliant.
|
|
926
|
+
*
|
|
927
|
+
* Creates a validator that implements the Standard Schema specification,
|
|
928
|
+
* enabling interoperability with tools like tRPC, TanStack Form, and other
|
|
929
|
+
* ecosystem libraries that consume Standard Schema-compliant validators.
|
|
930
|
+
*
|
|
931
|
+
* The returned function can be used both as a regular Ascertain validator
|
|
932
|
+
* (throws on error) and as a Standard Schema validator (returns result object).
|
|
933
|
+
*
|
|
934
|
+
* @template T - The type of data the schema validates.
|
|
935
|
+
* @param schema - The Ascertain schema to wrap.
|
|
936
|
+
* @returns A function that validates data, with a `~standard` property for Standard Schema compliance.
|
|
937
|
+
*
|
|
938
|
+
* @see https://standardschema.dev/
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```typescript
|
|
942
|
+
* import { standardSchema, or, optional } from 'ascertain';
|
|
943
|
+
*
|
|
944
|
+
* // Create a Standard Schema-compliant validator
|
|
945
|
+
* const userValidator = standardSchema({
|
|
946
|
+
* name: String,
|
|
947
|
+
* age: Number,
|
|
948
|
+
* role: or('admin', 'user'),
|
|
949
|
+
* email: optional(String),
|
|
950
|
+
* });
|
|
951
|
+
*
|
|
952
|
+
* // Use as regular Ascertain validator (throws on error)
|
|
953
|
+
* userValidator({ name: 'Alice', age: 30, role: 'admin' });
|
|
954
|
+
*
|
|
955
|
+
* // Use Standard Schema interface (returns result object)
|
|
956
|
+
* const result = userValidator['~standard'].validate(unknownData);
|
|
957
|
+
* if (result.issues) {
|
|
958
|
+
* console.log(result.issues);
|
|
959
|
+
* } else {
|
|
960
|
+
* console.log(result.value); // typed as User
|
|
961
|
+
* }
|
|
962
|
+
*
|
|
963
|
+
* // Works with tRPC, TanStack Form, etc.
|
|
964
|
+
* ```
|
|
965
|
+
*/
|
|
966
|
+
interface StandardSchemaFn<T> {
|
|
967
|
+
(data: T): void;
|
|
968
|
+
'~standard': StandardSchemaV1.Props<T, T>;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export const standardSchema = <T>(schema: Schema<T>): StandardSchemaFn<T> => {
|
|
972
|
+
const validator = compile(schema);
|
|
973
|
+
|
|
974
|
+
const fn = ((data: T) => {
|
|
975
|
+
if (!validator(data)) {
|
|
976
|
+
throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
|
|
977
|
+
}
|
|
978
|
+
}) as StandardSchemaFn<T>;
|
|
979
|
+
fn['~standard'] = {
|
|
980
|
+
version: 1 as const,
|
|
981
|
+
vendor: 'ascertain',
|
|
982
|
+
validate: (value: unknown): StandardSchemaV1.Result<T> => {
|
|
983
|
+
if (validator(value as T)) {
|
|
984
|
+
return { value: value as T };
|
|
985
|
+
}
|
|
986
|
+
return { issues: validator.issues };
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
return fn;
|
|
991
|
+
};
|