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/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>[]) => new Or(schemas);
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>[]) => new And(schemas);
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 Optional(schema);
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
- * Creates a schema that validates arrays with a specific length and type for each position.
174
- * This is useful for coordinate pairs, RGB values, or any fixed-structure data.
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
- * @template T - The type of data the operator validates (a tuple of types).
177
- * @param schemas - Schemas for each position in the tuple, in order.
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
- * import { tuple, ascertain } from 'ascertain';
183
- *
184
- * // 2D coordinate tuple
185
- * const pointSchema = tuple(Number, Number);
186
- * ascertain(pointSchema, [10, 20], "point"); // ✓ Valid
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 tuple = <T>(...schemas: Schema<T>[]) => new Tuple(schemas);
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 = typeof Buffer === 'undefined' ? (value: string) => atob(value) : (value: string) => Buffer.from(value, 'base64').toString('utf-8');
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.includes('.') || value.includes('e') || value.includes('E') ? parseFloat(value) : parseInt(value, 10);
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
- const codeGenCollectErrors = (errorsAlias: string, code: string, extra: string = '') => `try {${code}} catch (e) {${errorsAlias}.push(e.message);${extra}}`;
393
- const codeGenExpectNoErrors = (errorsAlias: string) => `if (${errorsAlias}.length !== 0) { throw new TypeError(${errorsAlias}.join('\\n')); }`;
394
- const codeGenExpectNonError = (valueAlias: string, path: string) =>
395
- `if (${valueAlias} instanceof Error) { throw new TypeError(\`\${${valueAlias}.message} for path "${path}".\`); }`;
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
- const name = (schema as { name?: string })?.name;
454
- const primitiveType =
455
- name === 'String'
456
- ? 'string'
457
- : name === 'Number'
458
- ? 'number'
459
- : name === 'Boolean'
460
- ? 'boolean'
461
- : name === 'BigInt'
462
- ? 'bigint'
463
- : name === 'Symbol'
464
- ? 'symbol'
465
- : null;
466
-
467
- if (primitiveType) {
468
- code.push(
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
- );
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
- } 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
- );
593
+ return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${emit!(message)} }`;
480
594
  } else {
481
- const index = context.register(schema);
482
- const registryAlias = context.unique('r');
483
- code.push(
484
- `const ${registryAlias} = ctx.registry[${index}];`,
485
- `if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { throw new TypeError(\`Invalid instance of \${${valueAlias}?.constructor?.name} for path "${path}", expected an instance of ${schema?.name}\`); }`,
486
- `if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { throw new TypeError(\`Invalid type \${${valueAlias}?.constructor?.name} for path "${path}", expected type ${schema?.name}\`); }`,
487
- `if (Number.isNaN(${valueAlias}?.valueOf?.())) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected a valid ${schema?.name}\`); }`,
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
- return code.join('\n');
491
- } else if (Array.isArray(schema)) {
641
+ }
642
+
643
+ if (typeof schema === 'function') {
492
644
  const valueAlias = context.unique('v');
493
- const code: string[] = [
494
- `const ${valueAlias} = ${valuePath};`,
495
- codeGenExpectNonNullable(valueAlias, path),
496
- codeGenExpectNonError(valueAlias, path),
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
- if (schema.length === 1) {
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
- ...schema.map(
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}.length > ${schema.length}) { throw new TypeError(\`Invalid tuple length \${${valueAlias}.length} for path "${path}", expected ${schema.length}.\`); }`,
686
+ `else if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { ${emit!(`\`Invalid type \${${valueAlias}?.constructor?.name}, expected type ${name}\``)} }`,
516
687
  );
517
- code.push(...schema.map((s, idx) => codeGenCollectErrors(errorsAlias, codeGen(s, context, `${valueAlias}[${idx}]`, `${path}[${idx}]`))));
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
- return code.join('\n');
523
- } else if (typeof schema === 'object' && schema !== null) {
524
- if (schema instanceof RegExp) {
525
- const valueAlias = context.unique('v');
526
- return `
527
- const ${valueAlias} = ${valuePath};
528
- ${codeGenExpectNonNullable(valueAlias, path)}
529
- ${codeGenExpectNonError(valueAlias, path)}
530
- if (!${schema.toString()}.test(String(${valueAlias}))) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected to match ${schema.toString()}\`); }
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
- codeGenExpectNonNullable(valueAlias, path),
537
- codeGenExpectObject(valueAlias, path, 'Object'),
538
- codeGenExpectNonError(valueAlias, path),
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 ($keys in schema) {
541
- const keysAlias = context.unique('k');
542
- const errorsAlias = context.unique('err');
543
- const kAlias = context.unique('k');
544
- code.push(`
545
- const ${keysAlias} = Object.keys(${valueAlias});
546
- const ${errorsAlias} = [];
547
- for (const ${kAlias} of ${keysAlias}) { ${codeGenCollectErrors(errorsAlias, codeGen(schema[$keys], context, kAlias, `${path}[\${${kAlias}}]`))} }
548
- ${codeGenExpectNoErrors(errorsAlias)}
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
- if ($values in schema) {
552
- const vAlias = context.unique('val');
553
- const kAlias = context.unique('k');
554
- const entriesAlias = context.unique('en');
555
- const errorsAlias = context.unique('err');
556
- code.push(`
557
- const ${entriesAlias} = Object.entries(${valueAlias});
558
- const ${errorsAlias} = [];
559
- for (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGenCollectErrors(errorsAlias, codeGen(schema[$values], context, vAlias, `${path}[\${${kAlias}}]`))} }
560
- ${codeGenExpectNoErrors(errorsAlias)}
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
- if ($strict in schema && schema[$strict]) {
564
- const keysAlias = context.unique('k');
565
- const kAlias = context.unique('k');
566
- const extraAlias = context.unique('ex');
567
- code.push(`const ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});`);
568
- code.push(`const ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));`);
569
- code.push(`if (${extraAlias}.length !== 0) { throw new TypeError(\`Extra properties: \${${extraAlias}}, are not allowed for path "${path}"\`); }`);
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
- } else if (typeof schema === 'symbol') {
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
- return `
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
- return `
588
- const ${valueAlias} = ${valuePath};
589
- if (${valueAlias} !== null && ${valueAlias} !== undefined ) { throw new TypeError(\`Invalid value \${JSON.stringify(${valueAlias})} for path "${path}", expected nullable\`); }
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
- * This function takes a schema definition and generates a JavaScript function
608
- * that can be used to validate data against the schema.
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 rootName - A name for the root of the data structure (used in error messages).
613
- * @returns A validation function that takes data as input and throws a TypeError if the data does not conform to the schema.
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, and, or } from 'ascertain';
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
- * const validateUser = compile(userSchema, 'User');
887
+ * // First-error mode (default) - fastest for invalid data
888
+ * const validate = compile(userSchema);
627
889
  *
628
- * // Valid data - no error thrown
629
- * validateUser({
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
- * // Invalid data - throws TypeError
637
- * try {
638
- * validateUser({
639
- * name: 123, // Invalid: should be string
640
- * age: 'thirty' // Invalid: should be number
641
- * });
642
- * } catch (error) {
643
- * console.error(error.message); // Detailed validation errors
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>, rootName: string) => {
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', rootName);
650
- const validator = new Function('ctx', 'data', code);
651
- return (data: T) => validator(context, data);
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, '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
- * }, 'UserData');
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, '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, 'Point');
1005
+ * ascertain(coordinateSchema, point);
713
1006
  * ```
714
1007
  */
715
- export const ascertain = <T>(schema: Schema<T>, data: T, rootName = '[root]') => {
716
- compile(schema, rootName)(data);
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, '[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, rootName = '[root]') => {
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, rootName);
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
+ };