ascertain 2.1.0 → 3.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/README.md +252 -114
- package/build/index.cjs +616 -183
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +215 -186
- package/build/index.js +577 -183
- package/build/index.js.map +1 -1
- package/package.json +15 -13
- package/src/index.ts +777 -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,79 @@ 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
|
+
const CHECK = Symbol.for('@@check');
|
|
22
|
+
|
|
23
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
24
|
+
|
|
25
|
+
interface OrShape<T> {
|
|
26
|
+
readonly schemas: Schema<T>[];
|
|
27
|
+
readonly [$op]: typeof OR;
|
|
28
|
+
}
|
|
29
|
+
interface AndShape<T> {
|
|
30
|
+
readonly schemas: Schema<T>[];
|
|
31
|
+
readonly [$op]: typeof AND;
|
|
32
|
+
}
|
|
33
|
+
interface OptionalShape<T> {
|
|
34
|
+
readonly schemas: Schema<T>[];
|
|
35
|
+
readonly [$op]: typeof OPTIONAL;
|
|
36
|
+
}
|
|
37
|
+
interface TupleShape<T> {
|
|
38
|
+
readonly schemas: Schema<T>[];
|
|
39
|
+
readonly [$op]: typeof TUPLE;
|
|
40
|
+
}
|
|
41
|
+
interface DiscriminatedShape<T> {
|
|
42
|
+
readonly schemas: Schema<T>[];
|
|
43
|
+
readonly [$op]: typeof DISCRIMINATED;
|
|
44
|
+
readonly key: string;
|
|
45
|
+
}
|
|
46
|
+
export interface CheckContext {
|
|
47
|
+
ref(value: unknown): string;
|
|
48
|
+
}
|
|
49
|
+
interface CheckShape {
|
|
50
|
+
readonly [$op]: typeof CHECK;
|
|
51
|
+
readonly compile: (value: string, ctx: CheckContext) => { check: string; message: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Tagged<T> = OrShape<T> | AndShape<T> | OptionalShape<T> | TupleShape<T> | DiscriminatedShape<T> | CheckShape;
|
|
55
|
+
|
|
56
|
+
const OrCtor = function <T>(this: OrShape<T>, schemas: Schema<T>[]) {
|
|
57
|
+
(this as Mutable<OrShape<T>>).schemas = schemas;
|
|
58
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): OrShape<T>; prototype: { [$op]: typeof OR } };
|
|
59
|
+
OrCtor.prototype[$op] = OR;
|
|
60
|
+
|
|
61
|
+
const AndCtor = function <T>(this: AndShape<T>, schemas: Schema<T>[]) {
|
|
62
|
+
(this as Mutable<AndShape<T>>).schemas = schemas;
|
|
63
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): AndShape<T>; prototype: { [$op]: typeof AND } };
|
|
64
|
+
AndCtor.prototype[$op] = AND;
|
|
65
|
+
|
|
66
|
+
const OptionalCtor = function <T>(this: OptionalShape<T>, schema: Schema<T>) {
|
|
67
|
+
(this as Mutable<OptionalShape<T>>).schemas = [schema];
|
|
68
|
+
} as unknown as { new <T>(schema: Schema<T>): OptionalShape<T>; prototype: { [$op]: typeof OPTIONAL } };
|
|
69
|
+
OptionalCtor.prototype[$op] = OPTIONAL;
|
|
70
|
+
|
|
71
|
+
const TupleCtor = function <T>(this: TupleShape<T>, schemas: Schema<T>[]) {
|
|
72
|
+
(this as Mutable<TupleShape<T>>).schemas = schemas;
|
|
73
|
+
} as unknown as { new <T>(schemas: Schema<T>[]): TupleShape<T>; prototype: { [$op]: typeof TUPLE } };
|
|
74
|
+
TupleCtor.prototype[$op] = TUPLE;
|
|
75
|
+
|
|
76
|
+
const DiscriminatedCtor = function <T>(this: DiscriminatedShape<T>, schemas: Schema<T>[], key: string) {
|
|
77
|
+
(this as Mutable<DiscriminatedShape<T>>).schemas = schemas;
|
|
78
|
+
(this as Mutable<DiscriminatedShape<T>>).key = key;
|
|
79
|
+
} as unknown as { new <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T>; prototype: { [$op]: typeof DISCRIMINATED } };
|
|
80
|
+
DiscriminatedCtor.prototype[$op] = DISCRIMINATED;
|
|
81
|
+
|
|
82
|
+
const CheckCtor = function (this: CheckShape, compileFn: CheckShape['compile']) {
|
|
83
|
+
(this as Mutable<CheckShape>).compile = compileFn;
|
|
84
|
+
} as unknown as { new (compileFn: CheckShape['compile']): CheckShape; prototype: { [$op]: typeof CHECK } };
|
|
85
|
+
CheckCtor.prototype[$op] = CHECK;
|
|
86
|
+
|
|
33
87
|
/**
|
|
34
88
|
* Represents a schema for validating data.
|
|
35
89
|
*
|
|
@@ -44,167 +98,128 @@ export type Schema<T> =
|
|
|
44
98
|
? Schema<A>[] | unknown
|
|
45
99
|
: unknown;
|
|
46
100
|
|
|
47
|
-
class Or<T> extends Operator<T> {}
|
|
48
101
|
/**
|
|
49
102
|
* 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
103
|
*/
|
|
79
|
-
export const or = <T>(...schemas: Schema<T>[]) =>
|
|
104
|
+
export const or = <T>(...schemas: Schema<T>[]): OrShape<T> => {
|
|
105
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
106
|
+
return new OrCtor(schemas);
|
|
107
|
+
};
|
|
80
108
|
|
|
81
|
-
class And<T> extends Operator<T> {}
|
|
82
109
|
/**
|
|
83
110
|
* 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
111
|
*/
|
|
110
|
-
export const and = <T>(...schemas: Schema<T>[]) =>
|
|
112
|
+
export const and = <T>(...schemas: Schema<T>[]): AndShape<T> => {
|
|
113
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
114
|
+
return new AndCtor(schemas);
|
|
115
|
+
};
|
|
111
116
|
|
|
112
|
-
class Optional<T> extends Operator<T> {
|
|
113
|
-
constructor(schema: Schema<T>) {
|
|
114
|
-
super([schema]);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
117
|
/**
|
|
118
118
|
* 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
119
|
*/
|
|
167
|
-
export const optional = <T>(schema: Schema<T>) => new
|
|
120
|
+
export const optional = <T>(schema: Schema<T>): OptionalShape<T> => new OptionalCtor(schema);
|
|
168
121
|
|
|
169
|
-
class Tuple<T> extends Operator<T> {}
|
|
170
122
|
/**
|
|
171
123
|
* Operator for validating data against a fixed-length tuple of schemas.
|
|
124
|
+
*/
|
|
125
|
+
export const tuple = <T>(...schemas: Schema<T>[]): TupleShape<T> => {
|
|
126
|
+
if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
|
|
127
|
+
return new TupleCtor(schemas);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Operator for validating data against a discriminated union.
|
|
172
132
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
133
|
+
* Optimizes validation by checking the discriminant field first and only
|
|
134
|
+
* validating the matching variant. More efficient than `or()` for unions
|
|
135
|
+
* where each variant has a common field with a unique literal value.
|
|
175
136
|
*
|
|
176
|
-
* @
|
|
177
|
-
* @param
|
|
178
|
-
* @returns A schema that validates data as a tuple with the specified structure.
|
|
137
|
+
* @param schemas - Array of object schemas, each with a discriminant field containing a literal value.
|
|
138
|
+
* @param key - The name of the discriminant field present in all variants.
|
|
179
139
|
*
|
|
180
140
|
* @example
|
|
181
141
|
* ```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
|
|
142
|
+
* const messageSchema = discriminated([
|
|
143
|
+
* { type: 'email', address: String },
|
|
144
|
+
* { type: 'sms', phone: String },
|
|
145
|
+
* { type: 'push', token: String },
|
|
146
|
+
* ], 'type');
|
|
205
147
|
* ```
|
|
206
148
|
*/
|
|
207
|
-
export const
|
|
149
|
+
export const discriminated = <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T> => {
|
|
150
|
+
if (schemas.length === 0) throw new TypeError('discriminated requires at least one schema');
|
|
151
|
+
return new DiscriminatedCtor(schemas, key);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const check = (
|
|
155
|
+
fnOrOpts: ((v: unknown) => boolean) | { compile: (value: string, ctx: CheckContext) => { check: string; message: string } },
|
|
156
|
+
message?: string,
|
|
157
|
+
): CheckShape => {
|
|
158
|
+
if (typeof fnOrOpts === 'function') {
|
|
159
|
+
return new CheckCtor((v, ctx) => {
|
|
160
|
+
const fnRef = ctx.ref(fnOrOpts);
|
|
161
|
+
return {
|
|
162
|
+
check: `!${fnRef}(${v})`,
|
|
163
|
+
message: message ? JSON.stringify(message) : `\`check failed for value \${${v}}\``,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return new CheckCtor(fnOrOpts.compile);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const min = (n: number, message?: string): CheckShape =>
|
|
171
|
+
new CheckCtor((v) => ({
|
|
172
|
+
check: `${v} < ${n}`,
|
|
173
|
+
message: message ? JSON.stringify(message) : `\`must be >= ${n}, got \${${v}}\``,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
export const max = (n: number, message?: string): CheckShape =>
|
|
177
|
+
new CheckCtor((v) => ({
|
|
178
|
+
check: `${v} > ${n}`,
|
|
179
|
+
message: message ? JSON.stringify(message) : `\`must be <= ${n}, got \${${v}}\``,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
export const integer = (message?: string): CheckShape =>
|
|
183
|
+
new CheckCtor((v) => ({
|
|
184
|
+
check: `!Number.isInteger(${v})`,
|
|
185
|
+
message: message ? JSON.stringify(message) : `\`must be an integer, got \${${v}}\``,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
export const minLength = (n: number, message?: string): CheckShape =>
|
|
189
|
+
new CheckCtor((v) => ({
|
|
190
|
+
check: `${v}.length < ${n}`,
|
|
191
|
+
message: message ? JSON.stringify(message) : `\`length must be >= ${n}, got \${${v}.length}\``,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
export const maxLength = (n: number, message?: string): CheckShape =>
|
|
195
|
+
new CheckCtor((v) => ({
|
|
196
|
+
check: `${v}.length > ${n}`,
|
|
197
|
+
message: message ? JSON.stringify(message) : `\`length must be <= ${n}, got \${${v}.length}\``,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
export const gt = (n: number, message?: string): CheckShape =>
|
|
201
|
+
new CheckCtor((v) => ({
|
|
202
|
+
check: `${v} <= ${n}`,
|
|
203
|
+
message: message ? JSON.stringify(message) : `\`must be > ${n}, got \${${v}}\``,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
export const lt = (n: number, message?: string): CheckShape =>
|
|
207
|
+
new CheckCtor((v) => ({
|
|
208
|
+
check: `${v} >= ${n}`,
|
|
209
|
+
message: message ? JSON.stringify(message) : `\`must be < ${n}, got \${${v}}\``,
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
export const multipleOf = (n: number, message?: string): CheckShape =>
|
|
213
|
+
new CheckCtor((v) => ({
|
|
214
|
+
check: `${v} % ${n} !== 0`,
|
|
215
|
+
message: message ? JSON.stringify(message) : `\`must be a multiple of ${n}, got \${${v}}\``,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
export const uniqueItems = (message?: string): CheckShape =>
|
|
219
|
+
new CheckCtor((v) => ({
|
|
220
|
+
check: `new Set(${v}).size !== ${v}.length`,
|
|
221
|
+
message: message ? JSON.stringify(message) : `\`must have unique items\``,
|
|
222
|
+
}));
|
|
208
223
|
|
|
209
224
|
/**
|
|
210
225
|
* Decodes a base64-encoded string to UTF-8.
|
|
@@ -214,7 +229,9 @@ export const tuple = <T>(...schemas: Schema<T>[]) => new Tuple(schemas);
|
|
|
214
229
|
* @param value - The base64-encoded string to decode.
|
|
215
230
|
* @returns The decoded UTF-8 string.
|
|
216
231
|
*/
|
|
217
|
-
export const fromBase64 =
|
|
232
|
+
export const fromBase64 =
|
|
233
|
+
/* c8 ignore next */
|
|
234
|
+
typeof Buffer === 'undefined' ? (value: string) => atob(value) : (value: string) => Buffer.from(value, 'base64').toString('utf-8');
|
|
218
235
|
|
|
219
236
|
const MULTIPLIERS = {
|
|
220
237
|
ms: 1,
|
|
@@ -277,7 +294,7 @@ export const as = {
|
|
|
277
294
|
return value[0] === '-' ? -result : result;
|
|
278
295
|
}
|
|
279
296
|
|
|
280
|
-
const result = value.
|
|
297
|
+
const result = value.trim() ? Number(value) : NaN;
|
|
281
298
|
return Number.isNaN(result) ? asError(`Invalid value ${value}, expected a valid number`) : result;
|
|
282
299
|
},
|
|
283
300
|
/**
|
|
@@ -361,6 +378,87 @@ export const as = {
|
|
|
361
378
|
},
|
|
362
379
|
};
|
|
363
380
|
|
|
381
|
+
const DATETIME_RE = /^\d{4}-[01]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
382
|
+
const TIME_FMT_RE = /^(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
383
|
+
const DURATION_RE = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
384
|
+
const EMAIL_RE = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
385
|
+
const IDN_EMAIL_RE =
|
|
386
|
+
/^[a-z0-9!#$%&'*+/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]+)*@(?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?\.)+[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?$/i;
|
|
387
|
+
const HOSTNAME_RE = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.?$/i;
|
|
388
|
+
const IDN_HOSTNAME_RE =
|
|
389
|
+
/^(?=.{1,253}\.?$)[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]{0,61}[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?(?:\.[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]{0,61}[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?)*\.?$/i;
|
|
390
|
+
const IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
391
|
+
const IPV6_RE =
|
|
392
|
+
/^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$/i;
|
|
393
|
+
const URI_RE =
|
|
394
|
+
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
395
|
+
const isUriRef = (s: string): boolean => URI_RE.test(s) || /^[a-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]*$/i.test(s);
|
|
396
|
+
const IRI_RE =
|
|
397
|
+
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)?$/i;
|
|
398
|
+
const isIriRef = (s: string): boolean => IRI_RE.test(s) || /^[a-z0-9\-._~:/?#\[\]@!$&'()*+,;=\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF%]*$/i.test(s);
|
|
399
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
400
|
+
const URI_TEMPLATE_RE =
|
|
401
|
+
/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i;
|
|
402
|
+
const JSON_POINTER_RE = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
|
|
403
|
+
const REL_JSON_POINTER_RE = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
|
|
404
|
+
|
|
405
|
+
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
406
|
+
const isValidDate = (s: string): boolean => {
|
|
407
|
+
const m = /^\d{4}-(\d{2})-(\d{2})$/.exec(s);
|
|
408
|
+
if (!m) return false;
|
|
409
|
+
const month = +m[1],
|
|
410
|
+
day = +m[2];
|
|
411
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
412
|
+
if (month === 2) {
|
|
413
|
+
const y = +s.slice(0, 4);
|
|
414
|
+
return day <= (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28);
|
|
415
|
+
}
|
|
416
|
+
return day <= DAYS[month];
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const isValidRegex = (s: string): boolean => {
|
|
420
|
+
try {
|
|
421
|
+
new RegExp(s);
|
|
422
|
+
return true;
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const regexFormat = (re: RegExp, name: string, message?: string): CheckShape =>
|
|
429
|
+
new CheckCtor((v, ctx) => ({
|
|
430
|
+
check: `!${ctx.ref(re)}.test(${v})`,
|
|
431
|
+
message: message ? JSON.stringify(message) : `\`must be a valid ${name}, got \${${v}}\``,
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
const fnFormat = (fn: (s: string) => boolean, name: string, message?: string): CheckShape =>
|
|
435
|
+
new CheckCtor((v, ctx) => ({
|
|
436
|
+
check: `!${ctx.ref(fn)}(${v})`,
|
|
437
|
+
message: message ? JSON.stringify(message) : `\`must be a valid ${name}, got \${${v}}\``,
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
export const format = {
|
|
441
|
+
dateTime: (message?: string): CheckShape => regexFormat(DATETIME_RE, 'date-time', message),
|
|
442
|
+
date: (message?: string): CheckShape => fnFormat(isValidDate, 'date', message),
|
|
443
|
+
time: (message?: string): CheckShape => regexFormat(TIME_FMT_RE, 'time', message),
|
|
444
|
+
duration: (message?: string): CheckShape => regexFormat(DURATION_RE, 'duration', message),
|
|
445
|
+
email: (message?: string): CheckShape => regexFormat(EMAIL_RE, 'email', message),
|
|
446
|
+
idnEmail: (message?: string): CheckShape => regexFormat(IDN_EMAIL_RE, 'idn-email', message),
|
|
447
|
+
hostname: (message?: string): CheckShape => regexFormat(HOSTNAME_RE, 'hostname', message),
|
|
448
|
+
idnHostname: (message?: string): CheckShape => regexFormat(IDN_HOSTNAME_RE, 'idn-hostname', message),
|
|
449
|
+
ipv4: (message?: string): CheckShape => regexFormat(IPV4_RE, 'ipv4', message),
|
|
450
|
+
ipv6: (message?: string): CheckShape => regexFormat(IPV6_RE, 'ipv6', message),
|
|
451
|
+
uri: (message?: string): CheckShape => regexFormat(URI_RE, 'uri', message),
|
|
452
|
+
uriReference: (message?: string): CheckShape => fnFormat(isUriRef, 'uri-reference', message),
|
|
453
|
+
iri: (message?: string): CheckShape => regexFormat(IRI_RE, 'iri', message),
|
|
454
|
+
iriReference: (message?: string): CheckShape => fnFormat(isIriRef, 'iri-reference', message),
|
|
455
|
+
uuid: (message?: string): CheckShape => regexFormat(UUID_RE, 'uuid', message),
|
|
456
|
+
uriTemplate: (message?: string): CheckShape => regexFormat(URI_TEMPLATE_RE, 'uri-template', message),
|
|
457
|
+
jsonPointer: (message?: string): CheckShape => regexFormat(JSON_POINTER_RE, 'json-pointer', message),
|
|
458
|
+
relativeJsonPointer: (message?: string): CheckShape => regexFormat(REL_JSON_POINTER_RE, 'relative-json-pointer', message),
|
|
459
|
+
regex: (message?: string): CheckShape => fnFormat(isValidRegex, 'regex', message),
|
|
460
|
+
};
|
|
461
|
+
|
|
364
462
|
/**
|
|
365
463
|
* A class representing the context for schema validation.
|
|
366
464
|
*
|
|
@@ -389,232 +487,395 @@ class Context {
|
|
|
389
487
|
}
|
|
390
488
|
}
|
|
391
489
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const codeGenExpectNonNullable = (valueAlias: string, path: string) =>
|
|
397
|
-
`if (${valueAlias} === null || ${valueAlias} === undefined) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected non-nullable.\`); }`;
|
|
398
|
-
const codeGenExpectObject = (valueAlias: string, path: string, instanceOf: string) =>
|
|
399
|
-
`if (typeof ${valueAlias} !== 'object') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected an instance of ${instanceOf}\`); }`;
|
|
400
|
-
const codeGenExpectArray = (valueAlias: string, path: string) =>
|
|
401
|
-
`if (!Array.isArray(${valueAlias})) { throw new TypeError(\`Invalid instance of \${${valueAlias}.constructor?.name} for path "${path}", expected an instance of Array.\`); }`;
|
|
402
|
-
|
|
403
|
-
const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, path: string): string => {
|
|
404
|
-
if (schema instanceof And) {
|
|
405
|
-
const valueAlias = context.unique('v');
|
|
406
|
-
const errorsAlias = context.unique('err');
|
|
407
|
-
const code = schema.schemas.map((s) => `try { ${codeGen(s, context, valueAlias, path)} } catch (e) { ${errorsAlias}.push(e.message); }`).join('\n');
|
|
408
|
-
return `// And
|
|
409
|
-
const ${errorsAlias} = [];
|
|
410
|
-
const ${valueAlias} = ${valuePath};
|
|
411
|
-
${code}
|
|
412
|
-
${codeGenExpectNoErrors(errorsAlias)}
|
|
413
|
-
`;
|
|
414
|
-
} else if (schema instanceof Or) {
|
|
415
|
-
const valueAlias = context.unique('v');
|
|
416
|
-
const errorsAlias = context.unique('err');
|
|
417
|
-
const code = schema.schemas
|
|
418
|
-
.map((s) => codeGen(s, context, valueAlias, path))
|
|
419
|
-
.reduceRight((result, code) => codeGenCollectErrors(errorsAlias, code, result), codeGenExpectNoErrors(errorsAlias));
|
|
420
|
-
return `// Or
|
|
421
|
-
const ${errorsAlias} = [];
|
|
422
|
-
const ${valueAlias} = ${valuePath};
|
|
423
|
-
${code}
|
|
424
|
-
`;
|
|
425
|
-
} else if (schema instanceof Optional) {
|
|
426
|
-
const valueAlias = context.unique('v');
|
|
427
|
-
return `// Optional
|
|
428
|
-
const ${valueAlias} = ${valuePath};
|
|
429
|
-
if (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen(schema.schemas[0], context, valueAlias, path)} }
|
|
430
|
-
`;
|
|
431
|
-
} else if (schema instanceof Tuple) {
|
|
432
|
-
const valueAlias = context.unique('v');
|
|
433
|
-
const errorsAlias = context.unique('err');
|
|
434
|
-
const code: string[] = [
|
|
435
|
-
'// Tuple',
|
|
436
|
-
`const ${valueAlias} = ${valuePath};`,
|
|
437
|
-
`const ${errorsAlias} = [];`,
|
|
438
|
-
codeGenExpectNonNullable(valueAlias, path),
|
|
439
|
-
codeGenExpectObject(valueAlias, path, 'Array'),
|
|
440
|
-
codeGenExpectArray(valueAlias, path),
|
|
441
|
-
`if (${valueAlias}.length !== ${schema.schemas.length}) { throw new TypeError(\`Invalid tuple length \${${valueAlias}.length} for path "${path}", expected ${schema.schemas.length}.\`); }`,
|
|
442
|
-
...schema.schemas.map((s, idx) => codeGenCollectErrors(errorsAlias, codeGen(s, context, `${valueAlias}[${idx}]`, `${path}[${idx}]`))),
|
|
443
|
-
codeGenExpectNoErrors(errorsAlias),
|
|
444
|
-
];
|
|
445
|
-
return code.join('\n');
|
|
446
|
-
} else if (typeof schema === 'function') {
|
|
447
|
-
const valueAlias = context.unique('v');
|
|
448
|
-
const code: string[] = [`const ${valueAlias} = ${valuePath};`, codeGenExpectNonNullable(valueAlias, path)];
|
|
449
|
-
if ((schema as unknown) !== Error && !(schema?.prototype instanceof Error)) {
|
|
450
|
-
code.push(codeGenExpectNonError(valueAlias, path));
|
|
451
|
-
}
|
|
490
|
+
type Mode =
|
|
491
|
+
| { fast: true; onFail?: string }
|
|
492
|
+
| { fast: false; firstError: true; issues: string; path: PropertyKey[]; pathExpr: string }
|
|
493
|
+
| { fast: false; firstError: false; issues: string; path: PropertyKey[]; pathExpr: string };
|
|
452
494
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
495
|
+
const isTagged = (schema: unknown): schema is Tagged<unknown> => (schema as Tagged<unknown>)?.[$op] !== undefined;
|
|
496
|
+
|
|
497
|
+
const childMode = (mode: Exclude<Mode, { fast: true }>, key: PropertyKey | { dynamic: string }): Mode => {
|
|
498
|
+
if (typeof key === 'object' && 'dynamic' in key) {
|
|
499
|
+
return {
|
|
500
|
+
fast: false,
|
|
501
|
+
firstError: mode.firstError,
|
|
502
|
+
issues: mode.issues,
|
|
503
|
+
path: mode.path,
|
|
504
|
+
pathExpr: `[${mode.path.map((k) => JSON.stringify(k)).join(',')}${mode.path.length ? ',' : ''}${key.dynamic}]`,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const newPath = [...mode.path, key];
|
|
508
|
+
return { fast: false, firstError: mode.firstError, issues: mode.issues, path: newPath, pathExpr: JSON.stringify(newPath) };
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const toLiteral = (value: unknown): string => (typeof value === 'bigint' ? `${value}n` : JSON.stringify(value));
|
|
512
|
+
|
|
513
|
+
const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode: Mode): string => {
|
|
514
|
+
const emit = mode.fast
|
|
515
|
+
? null
|
|
516
|
+
: mode.firstError
|
|
517
|
+
? (msg: string) => `${mode.issues} = [{ message: ${msg}, path: ${mode.pathExpr} }]; return ${mode.issues};`
|
|
518
|
+
: (msg: string) => `(${mode.issues} || (${mode.issues} = [])).push({ message: ${msg}, path: ${mode.pathExpr} });`;
|
|
519
|
+
const fail = mode.fast ? (mode.onFail ?? 'return false;') : '';
|
|
520
|
+
|
|
521
|
+
if (isTagged(schema)) {
|
|
522
|
+
const tag = schema[$op];
|
|
523
|
+
if (tag === AND) {
|
|
524
|
+
const valueAlias = context.unique('v');
|
|
525
|
+
const code = schema.schemas.map((s) => codeGen(s, context, valueAlias, mode)).join('\n');
|
|
526
|
+
return `const ${valueAlias} = ${valuePath};\n${code}`;
|
|
527
|
+
} else if (tag === OR) {
|
|
528
|
+
const valueAlias = context.unique('v');
|
|
529
|
+
const foundValid = context.unique('valid');
|
|
530
|
+
if (mode.fast) {
|
|
531
|
+
const branches = schema.schemas.map((s) => {
|
|
532
|
+
const branchValid = context.unique('valid');
|
|
533
|
+
const branchCode = codeGen(s, context, valueAlias, { ...mode, onFail: `${branchValid} = false;` });
|
|
534
|
+
return `if (!${foundValid}) { let ${branchValid} = true; ${branchCode} if (${branchValid}) { ${foundValid} = true; } }`;
|
|
535
|
+
});
|
|
536
|
+
return `const ${valueAlias} = ${valuePath};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { ${fail} }`;
|
|
537
|
+
} else if (mode.firstError) {
|
|
538
|
+
const firstBranchIssues = context.unique('iss');
|
|
539
|
+
const branches = schema.schemas.map((s, idx) => {
|
|
540
|
+
const branchIssues = context.unique('iss');
|
|
541
|
+
const branchCode = codeGen(s, context, valueAlias, {
|
|
542
|
+
fast: false,
|
|
543
|
+
firstError: true,
|
|
544
|
+
issues: branchIssues,
|
|
545
|
+
path: mode.path,
|
|
546
|
+
pathExpr: mode.pathExpr,
|
|
547
|
+
}).replace(new RegExp(`; return ${branchIssues};`, 'g'), ';');
|
|
548
|
+
if (idx === 0) {
|
|
549
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${firstBranchIssues} = ${branchIssues}; } }`;
|
|
550
|
+
}
|
|
551
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } }`;
|
|
552
|
+
});
|
|
553
|
+
return `const ${valueAlias} = ${valuePath};\nlet ${firstBranchIssues};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { return ${firstBranchIssues}; }`;
|
|
554
|
+
} else {
|
|
555
|
+
const localIssues = context.unique('iss');
|
|
556
|
+
const branches = schema.schemas.map((s) => {
|
|
557
|
+
const branchIssues = context.unique('iss');
|
|
558
|
+
const branchCode = codeGen(s, context, valueAlias, {
|
|
559
|
+
fast: false,
|
|
560
|
+
firstError: false,
|
|
561
|
+
issues: branchIssues,
|
|
562
|
+
path: mode.path,
|
|
563
|
+
pathExpr: mode.pathExpr,
|
|
564
|
+
});
|
|
565
|
+
return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${localIssues}.push(...${branchIssues}); } }`;
|
|
566
|
+
});
|
|
567
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${localIssues} = [];\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { (${mode.issues} || (${mode.issues} = [])).push(...${localIssues}); }`;
|
|
568
|
+
}
|
|
569
|
+
} else if (tag === OPTIONAL) {
|
|
570
|
+
const valueAlias = context.unique('v');
|
|
571
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen((schema as OptionalShape<T>).schemas[0], context, valueAlias, mode)} }`;
|
|
572
|
+
} else if (tag === TUPLE) {
|
|
573
|
+
const valueAlias = context.unique('v');
|
|
574
|
+
if (mode.fast) {
|
|
575
|
+
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')}`;
|
|
576
|
+
} else {
|
|
577
|
+
return [
|
|
578
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
579
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
580
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
|
|
581
|
+
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
582
|
+
`else if (${valueAlias}.length !== ${schema.schemas.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.schemas.length}\``)} }`,
|
|
583
|
+
`else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`,
|
|
584
|
+
].join('\n');
|
|
585
|
+
}
|
|
586
|
+
} else if (tag === CHECK) {
|
|
587
|
+
const valueAlias = context.unique('v');
|
|
588
|
+
const ref = (v: unknown) => `ctx.registry[${context.register(v)}]`;
|
|
589
|
+
const { check: cond, message } = (schema as CheckShape).compile(valueAlias, { ref });
|
|
590
|
+
if (mode.fast) {
|
|
591
|
+
return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${fail} }`;
|
|
475
592
|
}
|
|
476
|
-
|
|
477
|
-
code.push(
|
|
478
|
-
`if (typeof ${valueAlias} !== 'function') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected type Function\`); }`,
|
|
479
|
-
);
|
|
593
|
+
return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${emit!(message)} }`;
|
|
480
594
|
} else {
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
595
|
+
const { key, schemas } = schema as DiscriminatedShape<T>;
|
|
596
|
+
const valueAlias = context.unique('v');
|
|
597
|
+
const discriminantAlias = context.unique('d');
|
|
598
|
+
const keyStr = JSON.stringify(key);
|
|
599
|
+
|
|
600
|
+
const variants: { value: unknown; schema: Schema<T> }[] = [];
|
|
601
|
+
for (const s of schemas) {
|
|
602
|
+
if (typeof s !== 'object' || s === null || !(key in s)) {
|
|
603
|
+
throw new TypeError(`discriminated: each schema must have the discriminant key "${key}"`);
|
|
604
|
+
}
|
|
605
|
+
const discriminantValue = (s as Record<string, unknown>)[key];
|
|
606
|
+
if (typeof discriminantValue !== 'string' && typeof discriminantValue !== 'number' && typeof discriminantValue !== 'boolean') {
|
|
607
|
+
throw new TypeError(`discriminated: discriminant value must be a string, number, or boolean literal`);
|
|
608
|
+
}
|
|
609
|
+
variants.push({ value: discriminantValue, schema: s });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (mode.fast) {
|
|
613
|
+
const branches = variants.map(({ value, schema: s }) => {
|
|
614
|
+
const branchCode = codeGen(s, context, valueAlias, mode);
|
|
615
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
616
|
+
});
|
|
617
|
+
return [
|
|
618
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
619
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`,
|
|
620
|
+
`const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
621
|
+
branches.join(' else ') + ` else { ${fail} }`,
|
|
622
|
+
].join('\n');
|
|
623
|
+
} else {
|
|
624
|
+
const validValues = variants.map((v) => JSON.stringify(v.value)).join(', ');
|
|
625
|
+
const branches = variants.map(({ value, schema: s }) => {
|
|
626
|
+
const branchCode = codeGen(s, context, valueAlias, mode);
|
|
627
|
+
return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
|
|
628
|
+
});
|
|
629
|
+
return [
|
|
630
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
631
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
632
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an object\``)} }`,
|
|
633
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
634
|
+
`else {`,
|
|
635
|
+
` const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
|
|
636
|
+
` ${branches.join(' else ')} else { ${emit!(`\`Invalid discriminant value \${JSON.stringify(${discriminantAlias})}, expected one of: ${validValues}\``)} }`,
|
|
637
|
+
`}`,
|
|
638
|
+
].join('\n');
|
|
639
|
+
}
|
|
489
640
|
}
|
|
490
|
-
|
|
491
|
-
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (typeof schema === 'function') {
|
|
492
644
|
const valueAlias = context.unique('v');
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
codeGenExpectObject(valueAlias, path, 'Array'),
|
|
498
|
-
codeGenExpectArray(valueAlias, path),
|
|
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} = [];`);
|
|
645
|
+
const name = (schema as { name?: string })?.name;
|
|
646
|
+
const s = schema as unknown;
|
|
647
|
+
const primitiveType =
|
|
648
|
+
s === String ? 'string' : s === Number ? 'number' : s === Boolean ? 'boolean' : s === BigInt ? 'bigint' : s === Symbol ? 'symbol' : null;
|
|
505
649
|
|
|
506
|
-
|
|
650
|
+
if (mode.fast) {
|
|
651
|
+
if (primitiveType) {
|
|
652
|
+
const checks = [`typeof ${valueAlias} !== '${primitiveType}'`];
|
|
653
|
+
if (primitiveType === 'number') checks.push(`Number.isNaN(${valueAlias})`);
|
|
654
|
+
return `const ${valueAlias} = ${valuePath};\nif (${checks.join(' || ')}) { ${fail} }`;
|
|
655
|
+
} else if (name === 'Function') {
|
|
656
|
+
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== 'function') { ${fail} }`;
|
|
657
|
+
} else {
|
|
658
|
+
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
659
|
+
const index = context.register(schema);
|
|
660
|
+
const registryAlias = context.unique('r');
|
|
661
|
+
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} }`;
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const code: string[] = [`const ${valueAlias} = ${valuePath};`];
|
|
665
|
+
if (primitiveType) {
|
|
666
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
667
|
+
code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
668
|
+
code.push(`else if (typeof ${valueAlias} !== '${primitiveType}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type ${name}\``)} }`);
|
|
669
|
+
if (primitiveType === 'number')
|
|
670
|
+
code.push(`else if (Number.isNaN(${valueAlias})) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
|
|
671
|
+
} else if (name === 'Function') {
|
|
672
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
673
|
+
code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
674
|
+
code.push(`else if (typeof ${valueAlias} !== 'function') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type Function\``)} }`);
|
|
675
|
+
} else {
|
|
676
|
+
const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
|
|
677
|
+
const index = context.register(schema);
|
|
678
|
+
const registryAlias = context.unique('r');
|
|
679
|
+
code.push(`const ${registryAlias} = ctx.registry[${index}];`);
|
|
680
|
+
code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
|
|
681
|
+
if (!isError) code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
|
|
507
682
|
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
|
-
),
|
|
683
|
+
`else if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}?.constructor?.name}, expected an instance of ${name}\``)} }`,
|
|
512
684
|
);
|
|
513
|
-
} else {
|
|
514
685
|
code.push(
|
|
515
|
-
`if (${valueAlias}
|
|
686
|
+
`else if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { ${emit!(`\`Invalid type \${${valueAlias}?.constructor?.name}, expected type ${name}\``)} }`,
|
|
516
687
|
);
|
|
517
|
-
code.push(
|
|
688
|
+
code.push(`else if (Number.isNaN(${valueAlias}?.valueOf?.())) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
|
|
518
689
|
}
|
|
519
|
-
|
|
520
|
-
code.push(codeGenExpectNoErrors(errorsAlias));
|
|
690
|
+
return code.join('\n');
|
|
521
691
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const ${valueAlias} = ${valuePath}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (Array.isArray(schema)) {
|
|
695
|
+
const valueAlias = context.unique('v');
|
|
696
|
+
if (mode.fast) {
|
|
697
|
+
let code = `const ${valueAlias} = ${valuePath};\nif (!Array.isArray(${valueAlias})) { ${fail} }`;
|
|
698
|
+
if (schema.length === 1) {
|
|
699
|
+
const value = context.unique('val');
|
|
700
|
+
const key = context.unique('key');
|
|
701
|
+
code += `\nfor (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, mode)} }`;
|
|
702
|
+
} else if (schema.length > 1) {
|
|
703
|
+
code += `\nif (${valueAlias}.length > ${schema.length}) { ${fail} }`;
|
|
704
|
+
code += '\n' + schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, mode)).join('\n');
|
|
705
|
+
}
|
|
706
|
+
return code;
|
|
532
707
|
} else {
|
|
533
|
-
const valueAlias = context.unique('v');
|
|
534
708
|
const code: string[] = [
|
|
535
709
|
`const ${valueAlias} = ${valuePath};`,
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
710
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
711
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
712
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
|
|
713
|
+
`else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
|
|
539
714
|
];
|
|
540
|
-
if (
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const ${
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
715
|
+
if (schema.length > 0) {
|
|
716
|
+
const value = context.unique('val');
|
|
717
|
+
const key = context.unique('key');
|
|
718
|
+
if (schema.length === 1) {
|
|
719
|
+
// Dynamic key - use runtime concat
|
|
720
|
+
code.push(
|
|
721
|
+
`else { for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, childMode(mode, { dynamic: key }))} } }`,
|
|
722
|
+
);
|
|
723
|
+
} else {
|
|
724
|
+
code.push(
|
|
725
|
+
`else if (${valueAlias}.length > ${schema.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.length}\``)} }`,
|
|
726
|
+
);
|
|
727
|
+
code.push(`else { ${schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`);
|
|
728
|
+
}
|
|
550
729
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
`)
|
|
730
|
+
return code.join('\n');
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
735
|
+
if (schema instanceof RegExp) {
|
|
736
|
+
const valueAlias = context.unique('v');
|
|
737
|
+
if (mode.fast) {
|
|
738
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined || ${valueAlias} instanceof Error || !${schema.toString()}.test(String(${valueAlias}))) { ${fail} }`;
|
|
739
|
+
} else {
|
|
740
|
+
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
741
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
742
|
+
} else {
|
|
743
|
+
const valueAlias = context.unique('v');
|
|
744
|
+
if (mode.fast) {
|
|
745
|
+
let code = `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`;
|
|
746
|
+
if ($keys in schema) {
|
|
747
|
+
const keysAlias = context.unique('k');
|
|
748
|
+
const kAlias = context.unique('k');
|
|
749
|
+
code += `\nconst ${keysAlias} = Object.keys(${valueAlias});\nfor (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, mode)} }`;
|
|
750
|
+
}
|
|
751
|
+
if ($values in schema) {
|
|
752
|
+
const vAlias = context.unique('val');
|
|
753
|
+
const kAlias = context.unique('k');
|
|
754
|
+
const entriesAlias = context.unique('en');
|
|
755
|
+
code += `\nconst ${entriesAlias} = Object.entries(${valueAlias});\nfor (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, mode)} }`;
|
|
756
|
+
}
|
|
757
|
+
if ($strict in schema && schema[$strict]) {
|
|
758
|
+
const keysAlias = context.unique('k');
|
|
759
|
+
const kAlias = context.unique('k');
|
|
760
|
+
const extraAlias = context.unique('ex');
|
|
761
|
+
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} }`;
|
|
762
|
+
}
|
|
763
|
+
code +=
|
|
764
|
+
'\n' +
|
|
765
|
+
Object.entries(schema)
|
|
766
|
+
.map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, mode))
|
|
767
|
+
.join('\n');
|
|
768
|
+
return code;
|
|
769
|
+
} else {
|
|
770
|
+
const code: string[] = [
|
|
771
|
+
`const ${valueAlias} = ${valuePath};`,
|
|
772
|
+
`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
|
|
773
|
+
`else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} }`,
|
|
774
|
+
`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
|
|
775
|
+
'else {',
|
|
776
|
+
];
|
|
777
|
+
const innerCode: string[] = [];
|
|
778
|
+
if ($keys in schema) {
|
|
779
|
+
const keysAlias = context.unique('k');
|
|
780
|
+
const kAlias = context.unique('k');
|
|
781
|
+
innerCode.push(`const ${keysAlias} = Object.keys(${valueAlias});`);
|
|
782
|
+
// Dynamic key - use runtime concat
|
|
783
|
+
innerCode.push(`for (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, childMode(mode, { dynamic: kAlias }))} }`);
|
|
784
|
+
}
|
|
785
|
+
if ($values in schema) {
|
|
786
|
+
const vAlias = context.unique('val');
|
|
787
|
+
const kAlias = context.unique('k');
|
|
788
|
+
const entriesAlias = context.unique('en');
|
|
789
|
+
innerCode.push(`const ${entriesAlias} = Object.entries(${valueAlias});`);
|
|
790
|
+
// Dynamic key - use runtime concat
|
|
791
|
+
innerCode.push(
|
|
792
|
+
`for (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, childMode(mode, { dynamic: kAlias }))} }`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if ($strict in schema && schema[$strict]) {
|
|
796
|
+
const keysAlias = context.unique('k');
|
|
797
|
+
const kAlias = context.unique('k');
|
|
798
|
+
const extraAlias = context.unique('ex');
|
|
799
|
+
innerCode.push(`const ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});`);
|
|
800
|
+
innerCode.push(`const ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));`);
|
|
801
|
+
innerCode.push(`if (${extraAlias}.length !== 0) { ${emit!(`\`Extra properties: \${${extraAlias}}, are not allowed\``)} }`);
|
|
802
|
+
}
|
|
803
|
+
// Static keys - pre-register paths
|
|
804
|
+
innerCode.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, childMode(mode, key))));
|
|
805
|
+
code.push(innerCode.join('\n'), '}');
|
|
806
|
+
return code.join('\n');
|
|
570
807
|
}
|
|
571
|
-
code.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, `${path}.${key}`)));
|
|
572
|
-
return `${code.join('\n')}`;
|
|
573
808
|
}
|
|
574
|
-
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (typeof schema === 'symbol') {
|
|
575
812
|
const index = context.register(schema);
|
|
576
813
|
const valueAlias = context.unique('v');
|
|
577
814
|
const registryAlias = context.unique('r');
|
|
815
|
+
if (mode.fast) {
|
|
816
|
+
return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (typeof ${valueAlias} !== 'symbol' || ${valueAlias} !== ${registryAlias}) { ${fail} }`;
|
|
817
|
+
} else {
|
|
818
|
+
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()}\``)} }`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
578
821
|
|
|
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) {
|
|
822
|
+
if (schema === null || schema === undefined) {
|
|
586
823
|
const valueAlias = context.unique('v');
|
|
587
|
-
|
|
588
|
-
const ${valueAlias} = ${valuePath}
|
|
589
|
-
|
|
590
|
-
|
|
824
|
+
if (mode.fast) {
|
|
825
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${fail} }`;
|
|
826
|
+
} else {
|
|
827
|
+
return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected nullable\``)} }`;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const valueAlias = context.unique('v');
|
|
832
|
+
if (mode.fast) {
|
|
833
|
+
return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== '${typeof schema}' || ${valueAlias} !== ${toLiteral(schema)}) { ${fail} }`;
|
|
591
834
|
} else {
|
|
592
|
-
const valueAlias = context.unique('v');
|
|
593
835
|
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
|
-
`;
|
|
836
|
+
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
837
|
}
|
|
602
838
|
};
|
|
603
839
|
|
|
840
|
+
const emptyIssues: StandardSchemaV1.Issue[] = [];
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Validator function returned by compile().
|
|
844
|
+
* Returns true if valid, false if invalid.
|
|
845
|
+
* Access `.issues` property after validation to get error details.
|
|
846
|
+
*/
|
|
847
|
+
export interface Validator<T> {
|
|
848
|
+
(data: T): boolean;
|
|
849
|
+
issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Options for the compile function.
|
|
854
|
+
*/
|
|
855
|
+
export interface CompileOptions {
|
|
856
|
+
/**
|
|
857
|
+
* When true, collects all validation errors instead of stopping at the first.
|
|
858
|
+
* Default is false (first-error mode) for optimal performance.
|
|
859
|
+
*/
|
|
860
|
+
allErrors?: boolean;
|
|
861
|
+
}
|
|
862
|
+
|
|
604
863
|
/**
|
|
605
|
-
* Compiles a schema into a validation function.
|
|
864
|
+
* Compiles a schema into a high-performance validation function.
|
|
606
865
|
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
866
|
+
* By default uses first-error mode which stops at the first validation failure
|
|
867
|
+
* and returns immediately. This provides optimal performance for invalid data.
|
|
868
|
+
*
|
|
869
|
+
* Set `allErrors: true` to collect all validation errors (slower but more informative).
|
|
609
870
|
*
|
|
610
871
|
* @template T - The type of data the schema validates.
|
|
611
872
|
* @param schema - The schema to compile.
|
|
612
|
-
* @param
|
|
613
|
-
* @returns A
|
|
873
|
+
* @param options - Optional configuration (allErrors: boolean).
|
|
874
|
+
* @returns A validator function that returns boolean. Access `.issues` for error details.
|
|
614
875
|
*
|
|
615
876
|
* @example
|
|
616
877
|
* ```typescript
|
|
617
|
-
* import { compile, optional,
|
|
878
|
+
* import { compile, optional, or } from 'ascertain';
|
|
618
879
|
*
|
|
619
880
|
* const userSchema = {
|
|
620
881
|
* name: String,
|
|
@@ -623,32 +884,65 @@ if (${valueAlias} !== ${value}) { throw new TypeError(\`Invalid value \${JSON.st
|
|
|
623
884
|
* role: or('admin', 'user', 'guest')
|
|
624
885
|
* };
|
|
625
886
|
*
|
|
626
|
-
*
|
|
887
|
+
* // First-error mode (default) - fastest for invalid data
|
|
888
|
+
* const validate = compile(userSchema);
|
|
627
889
|
*
|
|
628
|
-
* //
|
|
629
|
-
*
|
|
630
|
-
* name: 'John Doe',
|
|
631
|
-
* age: 30,
|
|
632
|
-
* email: 'john@example.com',
|
|
633
|
-
* role: 'user'
|
|
634
|
-
* });
|
|
890
|
+
* // All-errors mode - collects all validation issues
|
|
891
|
+
* const validateAll = compile(userSchema, { allErrors: true });
|
|
635
892
|
*
|
|
636
|
-
* //
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* console.
|
|
893
|
+
* // Valid data
|
|
894
|
+
* if (validate({ name: 'John', age: 30, role: 'user' })) {
|
|
895
|
+
* console.log('Valid!');
|
|
896
|
+
* }
|
|
897
|
+
*
|
|
898
|
+
* // Invalid data - check .issues for details
|
|
899
|
+
* if (!validate({ name: 123, age: 'thirty' })) {
|
|
900
|
+
* console.log(validate.issues); // Array with first validation issue
|
|
644
901
|
* }
|
|
645
902
|
* ```
|
|
646
903
|
*/
|
|
647
|
-
export const compile = <T>(schema: Schema<T>,
|
|
904
|
+
export const compile = <T>(schema: Schema<T>, options?: CompileOptions): Validator<T> => {
|
|
905
|
+
const allErrors = options?.allErrors ?? false;
|
|
906
|
+
|
|
907
|
+
if (allErrors) {
|
|
908
|
+
const fastContext = new Context();
|
|
909
|
+
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
910
|
+
const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
|
|
911
|
+
|
|
912
|
+
const issueContext = new Context();
|
|
913
|
+
const issueCode = `let issues;\n${codeGen(schema, issueContext, 'data', { fast: false, firstError: false, issues: 'issues', path: [], pathExpr: '[]' })}\nreturn issues || [];`;
|
|
914
|
+
const issueValidator = new Function('ctx', `return (data) => {\n${issueCode}\n};`)(issueContext) as (data: T) => StandardSchemaV1.Issue[];
|
|
915
|
+
|
|
916
|
+
const validator = ((data: T): boolean => {
|
|
917
|
+
if (fastValidator(data)) {
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
validator.issues = issueValidator(data);
|
|
921
|
+
return false;
|
|
922
|
+
}) as Validator<T>;
|
|
923
|
+
validator.issues = emptyIssues;
|
|
924
|
+
return validator;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const fastContext = new Context();
|
|
928
|
+
const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
|
|
929
|
+
const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
|
|
930
|
+
|
|
648
931
|
const context = new Context();
|
|
649
|
-
const code = codeGen(schema, context, 'data',
|
|
650
|
-
const
|
|
651
|
-
|
|
932
|
+
const code = codeGen(schema, context, 'data', { fast: false, firstError: true, issues: 'issues', path: [], pathExpr: '[]' });
|
|
933
|
+
const firstErrorValidator = new Function('ctx', `return (data) => {\nlet issues;\n${code}\nreturn issues;\n};`)(context) as (
|
|
934
|
+
data: T,
|
|
935
|
+
) => StandardSchemaV1.Issue[] | undefined;
|
|
936
|
+
|
|
937
|
+
const validator = ((data: T): boolean => {
|
|
938
|
+
if (fastValidator(data)) {
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
validator.issues = firstErrorValidator(data)!;
|
|
942
|
+
return false;
|
|
943
|
+
}) as Validator<T>;
|
|
944
|
+
validator.issues = emptyIssues;
|
|
945
|
+
return validator;
|
|
652
946
|
};
|
|
653
947
|
|
|
654
948
|
/**
|
|
@@ -660,7 +954,6 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
660
954
|
* @template T - The type of data the schema validates.
|
|
661
955
|
* @param schema - The schema to validate against.
|
|
662
956
|
* @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
957
|
* @throws `{TypeError}` If the data does not conform to the schema.
|
|
665
958
|
*
|
|
666
959
|
* @example
|
|
@@ -682,7 +975,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
682
975
|
* };
|
|
683
976
|
*
|
|
684
977
|
* // Validate data - throws if invalid, otherwise continues silently
|
|
685
|
-
* ascertain(userSchema, userData
|
|
978
|
+
* ascertain(userSchema, userData);
|
|
686
979
|
* console.log('User data is valid!');
|
|
687
980
|
*
|
|
688
981
|
* // Example with invalid data
|
|
@@ -691,7 +984,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
691
984
|
* name: 'Bob',
|
|
692
985
|
* age: 'twenty-five', // Invalid: should be number
|
|
693
986
|
* active: true
|
|
694
|
-
* }
|
|
987
|
+
* });
|
|
695
988
|
* } catch (error) {
|
|
696
989
|
* console.error('Validation failed:', error.message);
|
|
697
990
|
* }
|
|
@@ -703,17 +996,20 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
|
|
|
703
996
|
* const numbersSchema = [Number];
|
|
704
997
|
* const numbers = [1, 2, 3, 4, 5];
|
|
705
998
|
*
|
|
706
|
-
* ascertain(numbersSchema, numbers
|
|
999
|
+
* ascertain(numbersSchema, numbers);
|
|
707
1000
|
*
|
|
708
1001
|
* // Tuple validation
|
|
709
1002
|
* const coordinateSchema = tuple(Number, Number);
|
|
710
1003
|
* const point = [10, 20];
|
|
711
1004
|
*
|
|
712
|
-
* ascertain(coordinateSchema, point
|
|
1005
|
+
* ascertain(coordinateSchema, point);
|
|
713
1006
|
* ```
|
|
714
1007
|
*/
|
|
715
|
-
export const ascertain = <T>(schema: Schema<T>, data: T
|
|
716
|
-
compile(schema
|
|
1008
|
+
export const ascertain = <T>(schema: Schema<T>, data: T) => {
|
|
1009
|
+
const validator = compile(schema);
|
|
1010
|
+
if (!validator(data)) {
|
|
1011
|
+
throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
|
|
1012
|
+
}
|
|
717
1013
|
};
|
|
718
1014
|
|
|
719
1015
|
/**
|
|
@@ -732,7 +1028,6 @@ export type ExtractShape<C, S> = {
|
|
|
732
1028
|
*
|
|
733
1029
|
* @template C - The type of the config object.
|
|
734
1030
|
* @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
1031
|
* @returns A validator function that takes a schema and returns the typed config subset.
|
|
737
1032
|
*
|
|
738
1033
|
* @example
|
|
@@ -745,7 +1040,7 @@ export type ExtractShape<C, S> = {
|
|
|
745
1040
|
* redis: { host: as.string(process.env.REDIS_HOST) },
|
|
746
1041
|
* };
|
|
747
1042
|
*
|
|
748
|
-
* const validate = createValidator(config
|
|
1043
|
+
* const validate = createValidator(config);
|
|
749
1044
|
*
|
|
750
1045
|
* // Consumer only validates what it needs
|
|
751
1046
|
* const { app, kafka } = validate({
|
|
@@ -758,9 +1053,111 @@ export type ExtractShape<C, S> = {
|
|
|
758
1053
|
* // redis is not accessible - TypeScript error
|
|
759
1054
|
* ```
|
|
760
1055
|
*/
|
|
761
|
-
export const createValidator = <C>(config: C
|
|
1056
|
+
export const createValidator = <C>(config: C) => {
|
|
762
1057
|
return <S extends Schema<Partial<C>>>(schema: S): ExtractShape<C, S> => {
|
|
763
|
-
ascertain(schema as Schema<C>, config
|
|
1058
|
+
ascertain(schema as Schema<C>, config);
|
|
764
1059
|
return config as ExtractShape<C, S>;
|
|
765
1060
|
};
|
|
766
1061
|
};
|
|
1062
|
+
|
|
1063
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
1064
|
+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export namespace StandardSchemaV1 {
|
|
1068
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
1069
|
+
readonly version: 1;
|
|
1070
|
+
readonly vendor: string;
|
|
1071
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
1072
|
+
readonly types?: { readonly input: Input; readonly output: Output };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
1076
|
+
|
|
1077
|
+
export interface SuccessResult<Output> {
|
|
1078
|
+
readonly value: Output;
|
|
1079
|
+
readonly issues?: undefined;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export interface FailureResult {
|
|
1083
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export interface Issue {
|
|
1087
|
+
readonly message: string;
|
|
1088
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export interface PathSegment {
|
|
1092
|
+
readonly key: PropertyKey;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Wraps an Ascertain schema to make it Standard Schema v1 compliant.
|
|
1098
|
+
*
|
|
1099
|
+
* Creates a validator that implements the Standard Schema specification,
|
|
1100
|
+
* enabling interoperability with tools like tRPC, TanStack Form, and other
|
|
1101
|
+
* ecosystem libraries that consume Standard Schema-compliant validators.
|
|
1102
|
+
*
|
|
1103
|
+
* The returned function can be used both as a regular Ascertain validator
|
|
1104
|
+
* (throws on error) and as a Standard Schema validator (returns result object).
|
|
1105
|
+
*
|
|
1106
|
+
* @template T - The type of data the schema validates.
|
|
1107
|
+
* @param schema - The Ascertain schema to wrap.
|
|
1108
|
+
* @returns A function that validates data, with a `~standard` property for Standard Schema compliance.
|
|
1109
|
+
*
|
|
1110
|
+
* @see https://standardschema.dev/
|
|
1111
|
+
*
|
|
1112
|
+
* @example
|
|
1113
|
+
* ```typescript
|
|
1114
|
+
* import { standardSchema, or, optional } from 'ascertain';
|
|
1115
|
+
*
|
|
1116
|
+
* // Create a Standard Schema-compliant validator
|
|
1117
|
+
* const userValidator = standardSchema({
|
|
1118
|
+
* name: String,
|
|
1119
|
+
* age: Number,
|
|
1120
|
+
* role: or('admin', 'user'),
|
|
1121
|
+
* email: optional(String),
|
|
1122
|
+
* });
|
|
1123
|
+
*
|
|
1124
|
+
* // Use as regular Ascertain validator (throws on error)
|
|
1125
|
+
* userValidator({ name: 'Alice', age: 30, role: 'admin' });
|
|
1126
|
+
*
|
|
1127
|
+
* // Use Standard Schema interface (returns result object)
|
|
1128
|
+
* const result = userValidator['~standard'].validate(unknownData);
|
|
1129
|
+
* if (result.issues) {
|
|
1130
|
+
* console.log(result.issues);
|
|
1131
|
+
* } else {
|
|
1132
|
+
* console.log(result.value); // typed as User
|
|
1133
|
+
* }
|
|
1134
|
+
*
|
|
1135
|
+
* // Works with tRPC, TanStack Form, etc.
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
interface StandardSchemaFn<T> {
|
|
1139
|
+
(data: T): void;
|
|
1140
|
+
'~standard': StandardSchemaV1.Props<T, T>;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export const standardSchema = <T>(schema: Schema<T>): StandardSchemaFn<T> => {
|
|
1144
|
+
const validator = compile(schema);
|
|
1145
|
+
|
|
1146
|
+
const fn = ((data: T) => {
|
|
1147
|
+
if (!validator(data)) {
|
|
1148
|
+
throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
|
|
1149
|
+
}
|
|
1150
|
+
}) as StandardSchemaFn<T>;
|
|
1151
|
+
fn['~standard'] = {
|
|
1152
|
+
version: 1 as const,
|
|
1153
|
+
vendor: 'ascertain',
|
|
1154
|
+
validate: (value: unknown): StandardSchemaV1.Result<T> => {
|
|
1155
|
+
if (validator(value as T)) {
|
|
1156
|
+
return { value: value as T };
|
|
1157
|
+
}
|
|
1158
|
+
return { issues: validator.issues };
|
|
1159
|
+
},
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
return fn;
|
|
1163
|
+
};
|