@webergency-utils/typechecker 0.1.9 → 0.2.1

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.
@@ -2,133 +2,519 @@ const report = (ctx, path, expected, value, message) => {
2
2
  ctx.success = false;
3
3
  ctx.errors.push({ path, value, error: message || expected });
4
4
  };
5
+ function isPlainObject(v) {
6
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
7
+ return false;
8
+ }
9
+ if (v instanceof Date || v instanceof RegExp || v instanceof Map || v instanceof Set) {
10
+ return false;
11
+ }
12
+ if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(v)) {
13
+ return false;
14
+ }
15
+ if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
16
+ return false;
17
+ }
18
+ const proto = Object.getPrototypeOf(v);
19
+ return proto === Object.prototype || proto === null;
20
+ }
21
+ function testRegex(regex, value) {
22
+ if (!regex.global && !regex.sticky) {
23
+ return regex.test(value);
24
+ }
25
+ const copy = new RegExp(regex.source, regex.flags);
26
+ return copy.test(value);
27
+ }
28
+ function isMultipleOfNumber(v, n) {
29
+ if (n === 0 || !Number.isFinite(n)) {
30
+ return false;
31
+ }
32
+ if (!Number.isFinite(v)) {
33
+ return true;
34
+ }
35
+ const q = v / n;
36
+ return Math.abs(q - Math.round(q)) <= 1e-8 * Math.max(1, Math.abs(q));
37
+ }
38
+ function shouldMutate(ctx) {
39
+ return ctx.mutate === true;
40
+ }
41
+ function wantsQuery(ctx) {
42
+ return ctx.from === 'query';
43
+ }
44
+ function wantsJsonRevive(ctx) {
45
+ return ctx.from === 'json' || ctx.from === 'query';
46
+ }
47
+ function pathKey(path) {
48
+ if (!path) {
49
+ return '';
50
+ }
51
+ const bracket = path.lastIndexOf('[');
52
+ const dot = path.lastIndexOf('.');
53
+ if (bracket > dot) {
54
+ const end = path.lastIndexOf(']');
55
+ if (end > bracket) {
56
+ return path.slice(bracket + 1, end);
57
+ }
58
+ }
59
+ if (dot >= 0) {
60
+ return path.slice(dot + 1);
61
+ }
62
+ return path;
63
+ }
64
+ function fromCustom(ctx, path, value, type) {
65
+ if (typeof ctx.from !== 'function') {
66
+ return value;
67
+ }
68
+ return ctx.from(pathKey(path), value, type);
69
+ }
70
+ /** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
71
+ export function coerceQueryNumber(v) {
72
+ if (typeof v === 'number') {
73
+ return v;
74
+ }
75
+ if (typeof v === 'string' && v.trim() !== '') {
76
+ const parsed = parseFloat(v);
77
+ if (!Number.isNaN(parsed)) {
78
+ return parsed;
79
+ }
80
+ }
81
+ return v;
82
+ }
83
+ /** Query-style boolean coercion — shared by `from: 'query'` and `transform.ToBoolean`. */
84
+ export function coerceQueryBoolean(v) {
85
+ if (typeof v === 'boolean') {
86
+ return v;
87
+ }
88
+ if (typeof v === 'string' || typeof v === 'number') {
89
+ const s = String(v).toLowerCase();
90
+ if (s === 'true' || s === '1' || s === 'yes' || s === 'on') {
91
+ return true;
92
+ }
93
+ if (s === 'false' || s === '0' || s === 'no' || s === 'off') {
94
+ return false;
95
+ }
96
+ }
97
+ return v;
98
+ }
99
+ /** JSON-wire Date revival (ISO / date-parseable strings only). */
100
+ export function coerceJsonDate(v) {
101
+ if (v instanceof Date && !Number.isNaN(v.getTime())) {
102
+ return v;
103
+ }
104
+ if (typeof v === 'string') {
105
+ const parsed = new Date(v);
106
+ if (!Number.isNaN(parsed.getTime())) {
107
+ return parsed;
108
+ }
109
+ }
110
+ return v;
111
+ }
112
+ /** Query-style Date coercion — shared by `from: 'query'` and `transform.ToDate`. */
113
+ export function coerceQueryDate(v) {
114
+ const fromJson = coerceJsonDate(v);
115
+ if (fromJson !== v) {
116
+ return fromJson;
117
+ }
118
+ if (v instanceof Date && !Number.isNaN(v.getTime())) {
119
+ return v;
120
+ }
121
+ if (typeof v === 'number' && Number.isFinite(v)) {
122
+ const parsed = new Date(v);
123
+ if (!Number.isNaN(parsed.getTime())) {
124
+ return parsed;
125
+ }
126
+ }
127
+ return v;
128
+ }
129
+ const EMAIL_LOCAL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
130
+ const EMAIL_DOMAIN_RE = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}$/i;
131
+ const IDN_EMAIL_LOCAL_RE = /^[\p{L}\p{N}.!#$%&'*+/=?^_`{|}~-]+$/u;
132
+ const IDN_EMAIL_DOMAIN_RE = /^(?=.{1,253}$)(?:(?!-)[\p{L}\p{N}-]{1,63}(?<!-)\.)+[\p{L}]{2,63}$/u;
133
+ const HOSTNAME_RE = /^(?=.{1,253}$)(?:localhost|(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63})$/i;
134
+ const IDN_HOSTNAME_RE = /^(?=.{1,253}$)(?:localhost|(?:(?!-)[\p{L}\p{N}-]{1,63}(?<!-)\.)+[\p{L}]{2,63})$/iu;
135
+ const URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s<>"{}|\\^`]*$/;
136
+ const IRI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\S+$/u;
137
+ const URI_TEMPLATE_RE = /^(?:[^{}\s]|\{[+#./;?&=,!@|]?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*(?::[1-9]\d{0,3}|\*)?(?:,(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*(?::[1-9]\d{0,3}|\*)?)*\})+$/;
138
+ function isEmail(value) {
139
+ if (value.length > 254) {
140
+ return false;
141
+ }
142
+ const at = value.lastIndexOf('@');
143
+ if (at < 1 || at !== value.indexOf('@') || at === value.length - 1) {
144
+ return false;
145
+ }
146
+ const local = value.slice(0, at);
147
+ const domain = value.slice(at + 1);
148
+ if (local.length > 64) {
149
+ return false;
150
+ }
151
+ if (local.startsWith('.') || local.endsWith('.') || local.includes('..')) {
152
+ return false;
153
+ }
154
+ if (!EMAIL_LOCAL_RE.test(local)) {
155
+ return false;
156
+ }
157
+ if (!EMAIL_DOMAIN_RE.test(domain)) {
158
+ return false;
159
+ }
160
+ return true;
161
+ }
162
+ function isIdnEmail(value) {
163
+ if (value.length > 254) {
164
+ return false;
165
+ }
166
+ const at = value.lastIndexOf('@');
167
+ if (at < 1 || at !== value.indexOf('@') || at === value.length - 1) {
168
+ return false;
169
+ }
170
+ const local = value.slice(0, at);
171
+ const domain = value.slice(at + 1);
172
+ if (local.length > 64) {
173
+ return false;
174
+ }
175
+ if (local.startsWith('.') || local.endsWith('.') || local.includes('..')) {
176
+ return false;
177
+ }
178
+ if (!IDN_EMAIL_LOCAL_RE.test(local)) {
179
+ return false;
180
+ }
181
+ if (!IDN_EMAIL_DOMAIN_RE.test(domain)) {
182
+ return false;
183
+ }
184
+ return true;
185
+ }
186
+ function isHostname(value) {
187
+ return HOSTNAME_RE.test(value);
188
+ }
189
+ function isIdnHostname(value) {
190
+ return IDN_HOSTNAME_RE.test(value);
191
+ }
192
+ function isUri(value) {
193
+ if (!URI_RE.test(value)) {
194
+ return false;
195
+ }
196
+ try {
197
+ // eslint-disable-next-line no-new
198
+ new URL(value);
199
+ return true;
200
+ }
201
+ catch {
202
+ // mailto:, urn:, and some other schemes are valid URIs but may throw in URL()
203
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s<>"{}|\\^`]+$/.test(value);
204
+ }
205
+ }
206
+ function isUriReference(value) {
207
+ if (value === '') {
208
+ return true;
209
+ }
210
+ if (isUri(value)) {
211
+ return true;
212
+ }
213
+ if (/[\s<>"{}|\\^`]/.test(value)) {
214
+ return false;
215
+ }
216
+ try {
217
+ // eslint-disable-next-line no-new
218
+ new URL(value, 'http://example.com');
219
+ return true;
220
+ }
221
+ catch {
222
+ return false;
223
+ }
224
+ }
225
+ function isIri(value) {
226
+ if (!IRI_RE.test(value)) {
227
+ return false;
228
+ }
229
+ if (/[\u0000-\u001F\u007F]/.test(value)) {
230
+ return false;
231
+ }
232
+ try {
233
+ // eslint-disable-next-line no-new
234
+ new URL(value);
235
+ return true;
236
+ }
237
+ catch {
238
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:\S+$/u.test(value);
239
+ }
240
+ }
241
+ function isIriReference(value) {
242
+ if (value === '') {
243
+ return true;
244
+ }
245
+ if (isIri(value)) {
246
+ return true;
247
+ }
248
+ if (/\s|[\u0000-\u001F\u007F]/.test(value)) {
249
+ return false;
250
+ }
251
+ try {
252
+ // eslint-disable-next-line no-new
253
+ new URL(value, 'http://example.com');
254
+ return true;
255
+ }
256
+ catch {
257
+ return /^[^<>"{}|\\^`]+$/u.test(value);
258
+ }
259
+ }
260
+ function isUriTemplate(value) {
261
+ if (!value || /\s/.test(value)) {
262
+ return false;
263
+ }
264
+ let depth = 0;
265
+ for (const ch of value) {
266
+ if (ch === '{') {
267
+ depth++;
268
+ }
269
+ else if (ch === '}') {
270
+ if (depth === 0) {
271
+ return false;
272
+ }
273
+ depth--;
274
+ }
275
+ }
276
+ if (depth !== 0) {
277
+ return false;
278
+ }
279
+ return URI_TEMPLATE_RE.test(value);
280
+ }
281
+ function parseFormatDate(value) {
282
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
283
+ return undefined;
284
+ }
285
+ const parsed = new Date(value);
286
+ if (Number.isNaN(parsed.getTime())) {
287
+ return undefined;
288
+ }
289
+ return parsed;
290
+ }
291
+ function parseFormatDateTime(value) {
292
+ if (typeof value !== 'string') {
293
+ return undefined;
294
+ }
295
+ const parsed = new Date(value);
296
+ if (Number.isNaN(parsed.getTime())) {
297
+ return undefined;
298
+ }
299
+ return parsed;
300
+ }
301
+ function stableStringify(value, seen = new WeakSet()) {
302
+ if (value === null || typeof value !== 'object') {
303
+ return JSON.stringify(value);
304
+ }
305
+ if (seen.has(value)) {
306
+ return undefined;
307
+ }
308
+ seen.add(value);
309
+ if (Array.isArray(value)) {
310
+ const parts = value.map(item => {
311
+ const s = stableStringify(item, seen);
312
+ return s === undefined ? '"[Circular]"' : s;
313
+ });
314
+ return `[${parts.join(',')}]`;
315
+ }
316
+ const keys = Object.keys(value).sort();
317
+ const parts = keys.map(key => `${JSON.stringify(key)}:${stableStringify(value[key], seen) ?? '"[Circular]"'}`);
318
+ return `{${parts.join(',')}}`;
319
+ }
5
320
  export const validators = {
321
+ coerceQueryNumber,
322
+ coerceQueryBoolean,
323
+ coerceQueryDate,
324
+ coerceJsonDate,
6
325
  string: (v, path, ctx) => {
7
- if (typeof v !== 'string') {
8
- report(ctx, path, 'Type<string>', v);
326
+ if (typeof v === 'string') {
327
+ return v;
9
328
  }
329
+ if (typeof ctx.from === 'function') {
330
+ const converted = fromCustom(ctx, path, v, 'string');
331
+ if (typeof converted === 'string') {
332
+ return converted;
333
+ }
334
+ }
335
+ report(ctx, path, 'Type<string>', v);
10
336
  return v;
11
337
  },
12
338
  number: (v, path, ctx) => {
13
- if (typeof v !== 'number') {
14
- if (ctx.tryConvert && typeof v === 'string' && v.trim() !== '') {
15
- const parsed = parseFloat(v);
16
- if (!isNaN(parsed)) {
17
- return parsed;
18
- }
339
+ if (wantsQuery(ctx)) {
340
+ v = coerceQueryNumber(v);
341
+ }
342
+ if (typeof v === 'number') {
343
+ if (Number.isNaN(v)) {
344
+ report(ctx, path, 'Type<number>', v);
19
345
  }
20
- report(ctx, path, 'Type<number>', v);
346
+ return v;
21
347
  }
348
+ if (typeof ctx.from === 'function') {
349
+ const converted = fromCustom(ctx, path, v, 'number');
350
+ if (typeof converted === 'number' && !Number.isNaN(converted)) {
351
+ return converted;
352
+ }
353
+ }
354
+ report(ctx, path, 'Type<number>', v);
22
355
  return v;
23
356
  },
24
357
  bigint: (v, path, ctx) => {
25
- if (typeof v !== 'bigint') {
26
- if (ctx.tryConvert && typeof v === 'string' && v.trim() !== '') {
27
- try {
28
- return BigInt(v);
29
- }
30
- catch (e) { /* ignore */ }
358
+ if (typeof v === 'bigint') {
359
+ return v;
360
+ }
361
+ if (wantsJsonRevive(ctx) && typeof v === 'string' && v.trim() !== '') {
362
+ try {
363
+ return BigInt(v);
31
364
  }
32
- report(ctx, path, 'Type<bigint>', v);
365
+ catch (e) { /* ignore */ }
33
366
  }
367
+ if (wantsQuery(ctx) && typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v)) {
368
+ try {
369
+ return BigInt(v);
370
+ }
371
+ catch (e) { /* ignore */ }
372
+ }
373
+ if (typeof ctx.from === 'function') {
374
+ const converted = fromCustom(ctx, path, v, 'bigint');
375
+ if (typeof converted === 'bigint') {
376
+ return converted;
377
+ }
378
+ }
379
+ report(ctx, path, 'Type<bigint>', v);
34
380
  return v;
35
381
  },
36
382
  boolean: (v, path, ctx) => {
37
- if (typeof v !== 'boolean') {
38
- if (ctx.tryConvert && (v === undefined || v === null)) {
39
- return false;
383
+ if (wantsQuery(ctx)) {
384
+ v = coerceQueryBoolean(v);
385
+ }
386
+ if (typeof v === 'boolean') {
387
+ return v;
388
+ }
389
+ if (typeof ctx.from === 'function') {
390
+ const converted = fromCustom(ctx, path, v, 'boolean');
391
+ if (typeof converted === 'boolean') {
392
+ return converted;
40
393
  }
41
- if (ctx.tryConvert && (typeof v === 'string' || typeof v === 'number')) {
42
- const s = String(v).toLowerCase();
43
- if (s === 'true' || s === '1' || s === 'yes' || s === 'on') {
44
- return true;
45
- }
46
- if (s === 'false' || s === '0' || s === 'no' || s === 'off') {
47
- return false;
48
- }
394
+ }
395
+ report(ctx, path, 'Type<boolean>', v);
396
+ return v;
397
+ },
398
+ function: (v, path, ctx) => {
399
+ if (typeof v === 'function') {
400
+ return v;
401
+ }
402
+ if (typeof ctx.from === 'function') {
403
+ const converted = fromCustom(ctx, path, v, 'function');
404
+ if (typeof converted === 'function') {
405
+ return converted;
49
406
  }
50
- report(ctx, path, 'Type<boolean>', v);
51
407
  }
408
+ report(ctx, path, 'Type<function>', v);
52
409
  return v;
53
410
  },
54
411
  date: (v, path, ctx) => {
55
- if (!(v instanceof Date) || isNaN(v.getTime())) {
56
- if (ctx.tryConvert && typeof v === 'string') {
57
- const parsed = new Date(v);
58
- if (!isNaN(parsed.getTime())) {
59
- return parsed;
60
- }
412
+ if (wantsQuery(ctx)) {
413
+ v = coerceQueryDate(v);
414
+ }
415
+ else if (ctx.from === 'json') {
416
+ v = coerceJsonDate(v);
417
+ }
418
+ if (v instanceof Date && !Number.isNaN(v.getTime())) {
419
+ return v;
420
+ }
421
+ if (typeof ctx.from === 'function') {
422
+ const converted = fromCustom(ctx, path, v, 'Date');
423
+ if (converted instanceof Date && !Number.isNaN(converted.getTime())) {
424
+ return converted;
61
425
  }
62
- report(ctx, path, 'Type<Date>', v);
63
426
  }
427
+ report(ctx, path, 'Type<Date>', v);
64
428
  return v;
65
429
  },
66
430
  regexp: (v, path, ctx) => {
67
- if (!(v instanceof RegExp)) {
68
- if (ctx.tryConvert && typeof v === 'string') {
431
+ if (v instanceof RegExp) {
432
+ return v;
433
+ }
434
+ if (wantsJsonRevive(ctx)) {
435
+ if (typeof v === 'string') {
69
436
  const match = v.match(/^\/(.*)\/([gimuy]*)$/);
70
437
  if (match) {
71
438
  try {
72
439
  return new RegExp(match[1], match[2]);
73
440
  }
74
- catch (e) { }
441
+ catch (e) { /* fall through */ }
75
442
  }
76
- else {
443
+ else if (wantsQuery(ctx)) {
77
444
  try {
78
445
  return new RegExp(v);
79
446
  }
80
- catch (e) { }
447
+ catch (e) { /* fall through */ }
448
+ }
449
+ }
450
+ if (v && typeof v === 'object' && typeof v.source === 'string') {
451
+ try {
452
+ return new RegExp(v.source, typeof v.flags === 'string' ? v.flags : '');
81
453
  }
454
+ catch (e) { /* fall through */ }
455
+ }
456
+ }
457
+ if (typeof ctx.from === 'function') {
458
+ const converted = fromCustom(ctx, path, v, 'RegExp');
459
+ if (converted instanceof RegExp) {
460
+ return converted;
82
461
  }
83
- report(ctx, path, 'Type<RegExp>', v);
84
462
  }
463
+ report(ctx, path, 'Type<RegExp>', v);
85
464
  return v;
86
465
  },
87
466
  null: (v, path, ctx) => {
88
- if (v !== null) {
89
- report(ctx, path, 'Type<null>', v);
467
+ if (v === null) {
468
+ return null;
90
469
  }
470
+ if (typeof ctx.from === 'function') {
471
+ const converted = fromCustom(ctx, path, v, 'null');
472
+ if (converted === null) {
473
+ return null;
474
+ }
475
+ }
476
+ report(ctx, path, 'Type<null>', v);
91
477
  return null;
92
478
  },
93
479
  undefined: (v, path, ctx) => {
94
- if (v !== undefined) {
95
- report(ctx, path, 'Type<undefined>', v);
480
+ if (v === undefined) {
481
+ return undefined;
96
482
  }
483
+ if (typeof ctx.from === 'function') {
484
+ const converted = fromCustom(ctx, path, v, 'undefined');
485
+ if (converted === undefined) {
486
+ return undefined;
487
+ }
488
+ }
489
+ report(ctx, path, 'Type<undefined>', v);
97
490
  return undefined;
98
491
  },
99
492
  literal: (v, path, ctx, expected) => {
100
- if (v !== expected) {
101
- if (ctx.tryConvert && (v === undefined || v === null)) {
102
- if (typeof expected === 'boolean') {
103
- if (expected === false) {
104
- return false;
105
- }
493
+ if (v === expected) {
494
+ return v;
495
+ }
496
+ if (wantsQuery(ctx)) {
497
+ if (typeof expected === 'number') {
498
+ const p = coerceQueryNumber(v);
499
+ if (p === expected) {
500
+ return p;
106
501
  }
107
502
  }
108
- if (ctx.tryConvert && typeof v === 'string') {
109
- if (typeof expected === 'number') {
110
- const p = parseFloat(v);
111
- if (p === expected) {
112
- return p;
113
- }
114
- }
115
- if (typeof expected === 'boolean') {
116
- const s = v.toLowerCase();
117
- let val;
118
- if (s === 'true' || s === '1' || s === 'yes' || s === 'on') {
119
- val = true;
120
- }
121
- else if (s === 'false' || s === '0' || s === 'no' || s === 'off') {
122
- val = false;
123
- }
124
- if (val === expected) {
125
- return val;
126
- }
503
+ if (typeof expected === 'boolean') {
504
+ const val = coerceQueryBoolean(v);
505
+ if (val === expected) {
506
+ return val;
127
507
  }
128
508
  }
129
- const expStr = typeof expected === 'string' ? `'${expected}'` : expected;
130
- report(ctx, path, `Literal<${expStr}>`, v);
131
509
  }
510
+ if (typeof ctx.from === 'function') {
511
+ const converted = fromCustom(ctx, path, v, 'literal');
512
+ if (converted === expected) {
513
+ return converted;
514
+ }
515
+ }
516
+ const expStr = typeof expected === 'string' ? `'${expected}'` : expected;
517
+ report(ctx, path, `Literal<${expStr}>`, v);
132
518
  return v;
133
519
  },
134
520
  array: (v, path, ctx, childValidator) => {
@@ -136,17 +522,26 @@ export const validators = {
136
522
  if (ctx.wrapArrays && v !== undefined && v !== null) {
137
523
  v = [v];
138
524
  }
525
+ else if (typeof ctx.from === 'function') {
526
+ const converted = fromCustom(ctx, path, v, 'Array');
527
+ if (Array.isArray(converted)) {
528
+ v = converted;
529
+ }
530
+ else {
531
+ report(ctx, path, 'Type<Array>', v);
532
+ return v;
533
+ }
534
+ }
139
535
  else {
140
536
  report(ctx, path, 'Type<Array>', v);
141
537
  return v;
142
538
  }
143
539
  }
144
- const data = ctx.mode === 'strip' ? [] : v;
540
+ const mutate = shouldMutate(ctx);
541
+ const data = mutate ? v : [];
145
542
  for (let i = 0; i < v.length; i++) {
146
543
  const val = childValidator(v[i], path + '[' + i + ']', ctx);
147
- if (ctx.mode === 'strip') {
148
- data.push(val);
149
- }
544
+ data[i] = val;
150
545
  }
151
546
  return data;
152
547
  },
@@ -154,20 +549,67 @@ export const validators = {
154
549
  for (const [key, isOptional, validator] of props) {
155
550
  const val = v[key];
156
551
  const oldErrors = ctx.errors.length;
552
+ const wasSuccess = ctx.success;
157
553
  const result = validator(val, path + '.' + key, ctx);
158
554
  if (ctx.success) {
159
555
  data[key] = result;
160
556
  }
161
557
  else if (isOptional && val === undefined) {
162
- ctx.success = true;
163
558
  ctx.errors.length = oldErrors;
559
+ ctx.success = wasSuccess;
560
+ }
561
+ }
562
+ },
563
+ objectShell: (v, ctx) => {
564
+ if (shouldMutate(ctx)) {
565
+ return v;
566
+ }
567
+ if (!isPlainObject(v)) {
568
+ return v;
569
+ }
570
+ if (ctx.mode === 'strip') {
571
+ return {};
572
+ }
573
+ return { ...v };
574
+ },
575
+ stripExtras: (data, ctx, allowedKeys) => {
576
+ if (!shouldMutate(ctx) || ctx.mode !== 'strip' || !allowedKeys || !data || typeof data !== 'object') {
577
+ return data;
578
+ }
579
+ for (const k of Object.keys(data)) {
580
+ if (!allowedKeys.includes(k)) {
581
+ delete data[k];
582
+ }
583
+ }
584
+ return data;
585
+ },
586
+ additionalProps: (v, data, path, ctx, knownKeys, childValidator) => {
587
+ if (!isPlainObject(v)) {
588
+ return;
589
+ }
590
+ for (const key of Object.keys(v)) {
591
+ if (knownKeys.includes(key)) {
592
+ continue;
164
593
  }
594
+ data[key] = childValidator(v[key], path + '.' + key, ctx);
165
595
  }
166
596
  },
167
597
  object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
168
- if (!v || typeof v !== 'object' || Array.isArray(v)) {
169
- report(ctx, path, expected, v);
170
- return false;
598
+ if (!isPlainObject(v)) {
599
+ if (typeof ctx.from === 'function') {
600
+ const converted = fromCustom(ctx, path, v, 'Object');
601
+ if (isPlainObject(converted)) {
602
+ v = converted;
603
+ }
604
+ else {
605
+ report(ctx, path, expected, v);
606
+ return false;
607
+ }
608
+ }
609
+ else {
610
+ report(ctx, path, expected, v);
611
+ return false;
612
+ }
171
613
  }
172
614
  if (ctx.mode === 'strict' && allowedKeys) {
173
615
  for (const k of Object.keys(v)) {
@@ -176,10 +618,10 @@ export const validators = {
176
618
  }
177
619
  }
178
620
  }
179
- return true;
621
+ return v;
180
622
  },
181
623
  templateLiteral: (v, path, ctx, regex, expected) => {
182
- if (typeof v !== 'string' || !regex.test(v)) {
624
+ if (typeof v !== 'string' || !testRegex(regex, v)) {
183
625
  report(ctx, path, expected, v);
184
626
  }
185
627
  return v;
@@ -226,15 +668,13 @@ export const validators = {
226
668
  report(ctx, path, `MultipleOf<${n}>`, v, message);
227
669
  }
228
670
  }
229
- else {
230
- if (v % n !== 0) {
231
- report(ctx, path, `MultipleOf<${n}>`, v, message);
232
- }
671
+ else if (!isMultipleOfNumber(v, n)) {
672
+ report(ctx, path, `MultipleOf<${n}>`, v, message);
233
673
  }
234
674
  return v;
235
675
  },
236
676
  pattern: (v, path, ctx, regex, expected, message) => {
237
- if (!regex.test(v)) {
677
+ if (!testRegex(regex, v)) {
238
678
  report(ctx, path, expected, v, message);
239
679
  }
240
680
  return v;
@@ -242,9 +682,13 @@ export const validators = {
242
682
  format: (v, path, ctx, format, message) => {
243
683
  let regex;
244
684
  let isValid = true;
685
+ let result = v;
245
686
  switch (format) {
246
687
  case 'email':
247
- regex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
688
+ isValid = isEmail(v);
689
+ break;
690
+ case 'idn-email':
691
+ isValid = isIdnEmail(v);
248
692
  break;
249
693
  case 'uuid':
250
694
  regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -259,11 +703,27 @@ export const validators = {
259
703
  regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
260
704
  break;
261
705
  case 'date':
262
- regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
263
- break;
706
+ {
707
+ const parsed = parseFormatDate(v);
708
+ if (!parsed) {
709
+ isValid = false;
710
+ }
711
+ else if (wantsQuery(ctx)) {
712
+ result = parsed;
713
+ }
714
+ break;
715
+ }
264
716
  case 'date-time':
265
- regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[tT ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[zZ]|[+-]\d{2}:\d{2})$/;
266
- break;
717
+ {
718
+ const parsed = parseFormatDateTime(v);
719
+ if (!parsed) {
720
+ isValid = false;
721
+ }
722
+ else if (wantsQuery(ctx)) {
723
+ result = parsed;
724
+ }
725
+ break;
726
+ }
267
727
  case 'byte':
268
728
  regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
269
729
  break;
@@ -278,10 +738,25 @@ export const validators = {
278
738
  ;
279
739
  break;
280
740
  case 'hostname':
281
- regex = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}$/;
741
+ isValid = isHostname(v);
742
+ break;
743
+ case 'idn-hostname':
744
+ isValid = isIdnHostname(v);
282
745
  break;
283
746
  case 'uri':
284
- regex = /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/;
747
+ isValid = isUri(v);
748
+ break;
749
+ case 'uri-reference':
750
+ isValid = isUriReference(v);
751
+ break;
752
+ case 'iri':
753
+ isValid = isIri(v);
754
+ break;
755
+ case 'iri-reference':
756
+ isValid = isIriReference(v);
757
+ break;
758
+ case 'uri-template':
759
+ isValid = isUriTemplate(v);
285
760
  break;
286
761
  case 'time':
287
762
  regex = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[zZ]|[+-]\d{2}:\d{2})$/;
@@ -292,15 +767,17 @@ export const validators = {
292
767
  case 'objectId':
293
768
  regex = /^[0-9a-fA-F]{24}$/;
294
769
  break;
295
- default: break;
770
+ default:
771
+ isValid = false;
772
+ break;
296
773
  }
297
- if (regex && !regex.test(v)) {
774
+ if (regex && !testRegex(regex, v)) {
298
775
  isValid = false;
299
776
  }
300
777
  if (!isValid) {
301
778
  report(ctx, path, `Format<${format}>`, v, message);
302
779
  }
303
- return v;
780
+ return result;
304
781
  },
305
782
  minItems: (v, path, ctx, min, message) => {
306
783
  if (v.length < min) {
@@ -318,7 +795,9 @@ export const validators = {
318
795
  const seen = new Set();
319
796
  for (let i = 0; i < v.length; i++) {
320
797
  const item = v[i];
321
- const key = typeof item === 'object' && item !== null ? JSON.stringify(item) : item;
798
+ const key = typeof item === 'object' && item !== null
799
+ ? stableStringify(item) ?? item
800
+ : item;
322
801
  if (seen.has(key)) {
323
802
  report(ctx, path, 'UniqueItems', v, message);
324
803
  break;
@@ -328,58 +807,107 @@ export const validators = {
328
807
  return v;
329
808
  },
330
809
  custom: (v, path, ctx, fn, message) => {
331
- const parentPath = path.includes('.') ? path.substring(0, path.lastIndexOf('.')) : '';
810
+ const pathParts = tokenizePath(path);
811
+ const parentPath = joinPathSegments(pathParts.slice(0, -1));
332
812
  const parent = getValueAtPath(ctx.root, parentPath);
333
- const indexMatch = path.match(/\[(\d+)\]$/);
334
- const index = indexMatch ? parseInt(indexMatch[1], 10) : undefined;
335
- if (!fn(v, { parent, root: ctx.root, path, index })) {
813
+ const last = pathParts[pathParts.length - 1];
814
+ const index = last && last.startsWith('[') ? parseInt(last.slice(1, -1), 10) : undefined;
815
+ if (!fn(v, { parent, root: ctx.root, path, index: Number.isNaN(index) ? undefined : index })) {
336
816
  report(ctx, path, fn.name ? `Custom<${fn.name}>` : 'Custom', v, message);
337
817
  }
338
818
  return v;
339
819
  },
340
820
  union: (v, path, ctx, checks, expected = 'Type<Union>') => {
821
+ const unionErrors = [];
341
822
  // Pass 1: No conversion
342
823
  for (const check of checks) {
343
- const subCtx = { ...ctx, success: true, errors: [], tryConvert: false };
824
+ const subCtx = { ...ctx, success: true, errors: [], from: undefined };
344
825
  const val = check(v, path, subCtx);
345
826
  if (subCtx.success) {
346
827
  return val;
347
828
  }
829
+ unionErrors.push(...subCtx.errors);
348
830
  }
349
- // Pass 2: With conversion
350
- const unionErrors = [];
351
- for (const check of checks) {
352
- const subCtx = { ...ctx, success: true, errors: [], tryConvert: true };
353
- const val = check(v, path, subCtx);
354
- if (subCtx.success) {
355
- return val;
831
+ // Pass 2: With conversion (only when caller opted in)
832
+ if (ctx.from) {
833
+ unionErrors.length = 0;
834
+ for (const check of checks) {
835
+ const subCtx = { ...ctx, success: true, errors: [] };
836
+ const val = check(v, path, subCtx);
837
+ if (subCtx.success) {
838
+ return val;
839
+ }
840
+ unionErrors.push(...subCtx.errors);
356
841
  }
357
- unionErrors.push(...subCtx.errors);
358
842
  }
359
843
  ctx.success = false;
360
844
  ctx.errors.push({
361
845
  path,
362
846
  value: v,
363
- error: expected
847
+ error: expected,
848
+ issues: unionErrors.length > 0 ? unionErrors : undefined
364
849
  });
365
- ctx.errors.push(...unionErrors);
366
850
  return v;
367
851
  },
368
852
  tuple: (v, path, ctx, checks) => {
369
853
  if (!Array.isArray(v) || v.length !== checks.length) {
370
- report(ctx, path, `Tuple<${checks.length}>`, v);
371
- return v;
854
+ if (typeof ctx.from === 'function') {
855
+ const converted = fromCustom(ctx, path, v, 'tuple');
856
+ if (Array.isArray(converted) && converted.length === checks.length) {
857
+ v = converted;
858
+ }
859
+ else {
860
+ report(ctx, path, `Tuple<${checks.length}>`, v);
861
+ return v;
862
+ }
863
+ }
864
+ else {
865
+ report(ctx, path, `Tuple<${checks.length}>`, v);
866
+ return v;
867
+ }
372
868
  }
373
- const data = ctx.mode === 'strip' ? [] : v;
869
+ const mutate = shouldMutate(ctx);
870
+ const data = mutate ? v : [];
374
871
  for (let i = 0; i < checks.length; i++) {
375
- const val = checks[i](v[i], path + '[' + i + ']', ctx);
376
- if (ctx.mode === 'strip') {
377
- data.push(val);
378
- }
872
+ data[i] = checks[i](v[i], path + '[' + i + ']', ctx);
379
873
  }
380
874
  return data;
381
875
  },
382
876
  any: (v) => v,
877
+ never: (v, path, ctx) => {
878
+ if (typeof ctx.from === 'function') {
879
+ fromCustom(ctx, path, v, 'never');
880
+ }
881
+ report(ctx, path, 'Type<never>', v);
882
+ return v;
883
+ },
884
+ symbol: (v, path, ctx) => {
885
+ if (typeof v === 'symbol') {
886
+ return v;
887
+ }
888
+ if (typeof ctx.from === 'function') {
889
+ const converted = fromCustom(ctx, path, v, 'symbol');
890
+ if (typeof converted === 'symbol') {
891
+ return converted;
892
+ }
893
+ }
894
+ report(ctx, path, 'Type<symbol>', v);
895
+ return v;
896
+ },
897
+ instanceOf: (v, path, ctx, typeName) => {
898
+ const ctor = globalThis[typeName];
899
+ if (ctor && v instanceof ctor) {
900
+ return v;
901
+ }
902
+ if (typeof ctx.from === 'function') {
903
+ const converted = fromCustom(ctx, path, v, 'instance');
904
+ if (ctor && converted instanceof ctor) {
905
+ return converted;
906
+ }
907
+ }
908
+ report(ctx, path, `Type<${typeName}>`, v);
909
+ return v;
910
+ },
383
911
  requires: (v, path, ctx, reqs, message) => {
384
912
  if (v === undefined || v === null) {
385
913
  return v;
@@ -393,64 +921,151 @@ export const validators = {
393
921
  return v;
394
922
  },
395
923
  record: (v, path, ctx, childValidator) => {
396
- if (!v || typeof v !== 'object' || Array.isArray(v)) {
397
- report(ctx, path, 'Type<Object>', v);
398
- return v;
924
+ if (!isPlainObject(v)) {
925
+ if (typeof ctx.from === 'function') {
926
+ const converted = fromCustom(ctx, path, v, 'Object');
927
+ if (isPlainObject(converted)) {
928
+ v = converted;
929
+ }
930
+ else {
931
+ report(ctx, path, 'Type<Object>', v);
932
+ return v;
933
+ }
934
+ }
935
+ else {
936
+ report(ctx, path, 'Type<Object>', v);
937
+ return v;
938
+ }
399
939
  }
400
- const data = ctx.mode === 'strip' ? {} : v;
940
+ const mutate = shouldMutate(ctx);
941
+ const data = mutate ? v : {};
401
942
  for (const key of Object.keys(v)) {
402
- const val = childValidator(v[key], path + '.' + key, ctx);
403
- if (ctx.mode === 'strip') {
404
- data[key] = val;
405
- }
943
+ data[key] = childValidator(v[key], path + '.' + key, ctx);
406
944
  }
407
945
  return data;
408
946
  },
409
947
  set: (v, path, ctx, childValidator, message) => {
410
948
  if (!(v instanceof Set)) {
411
- if (ctx.tryConvert && Array.isArray(v)) {
949
+ if (wantsJsonRevive(ctx) && Array.isArray(v)) {
412
950
  v = new Set(v);
413
951
  }
414
- else if (ctx.tryConvert && v !== undefined && v !== null) {
952
+ else if (wantsQuery(ctx) && v !== undefined && v !== null) {
415
953
  v = new Set([v]);
416
954
  }
955
+ else if (typeof ctx.from === 'function') {
956
+ const converted = fromCustom(ctx, path, v, 'Set');
957
+ if (converted instanceof Set) {
958
+ v = converted;
959
+ }
960
+ else {
961
+ report(ctx, path, 'Type<Set>', v, message);
962
+ return v;
963
+ }
964
+ }
417
965
  else {
418
966
  report(ctx, path, 'Type<Set>', v, message);
419
967
  return v;
420
968
  }
421
969
  }
422
- const data = ctx.mode === 'strip' ? new Set() : v;
970
+ const mutate = shouldMutate(ctx);
971
+ const source = [...v];
972
+ if (mutate) {
973
+ v.clear();
974
+ }
975
+ const data = mutate ? v : new Set();
423
976
  let index = 0;
424
- for (const item of v) {
425
- const val = childValidator(item, `${path}[${index}]`, ctx);
426
- if (ctx.mode === 'strip') {
427
- data.add(val);
428
- }
977
+ for (const item of source) {
978
+ data.add(childValidator(item, `${path}[${index}]`, ctx));
429
979
  index++;
430
980
  }
431
981
  return data;
432
982
  },
433
983
  map: (v, path, ctx, keyValidator, valueValidator, message) => {
434
984
  if (!(v instanceof Map)) {
435
- if (ctx.tryConvert && typeof v === 'object' && v !== null && !Array.isArray(v)) {
985
+ if (wantsJsonRevive(ctx) && isPlainObject(v)) {
436
986
  v = new Map(Object.entries(v));
437
987
  }
988
+ else if (typeof ctx.from === 'function') {
989
+ const converted = fromCustom(ctx, path, v, 'Map');
990
+ if (converted instanceof Map) {
991
+ v = converted;
992
+ }
993
+ else {
994
+ report(ctx, path, 'Type<Map>', v, message);
995
+ return v;
996
+ }
997
+ }
438
998
  else {
439
999
  report(ctx, path, 'Type<Map>', v, message);
440
1000
  return v;
441
1001
  }
442
1002
  }
443
- const data = ctx.mode === 'strip' ? new Map() : v;
444
- for (const [key, val] of v.entries()) {
1003
+ const mutate = shouldMutate(ctx);
1004
+ const source = [...v.entries()];
1005
+ if (mutate) {
1006
+ v.clear();
1007
+ }
1008
+ const data = mutate ? v : new Map();
1009
+ for (const [key, val] of source) {
445
1010
  const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
446
1011
  const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
447
- if (ctx.mode === 'strip') {
448
- data.set(validatedKey, validatedVal);
449
- }
1012
+ data.set(validatedKey, validatedVal);
450
1013
  }
451
1014
  return data;
452
1015
  }
453
1016
  };
1017
+ function tokenizePath(path) {
1018
+ const cleanPath = path.startsWith('.') ? path.substring(1) : path;
1019
+ if (!cleanPath) {
1020
+ return [];
1021
+ }
1022
+ const segments = [];
1023
+ let buf = '';
1024
+ for (let i = 0; i < cleanPath.length; i++) {
1025
+ const ch = cleanPath[i];
1026
+ if (ch === '.') {
1027
+ if (buf) {
1028
+ segments.push(buf);
1029
+ buf = '';
1030
+ }
1031
+ }
1032
+ else if (ch === '[') {
1033
+ if (buf) {
1034
+ segments.push(buf);
1035
+ buf = '';
1036
+ }
1037
+ const end = cleanPath.indexOf(']', i);
1038
+ if (end === -1) {
1039
+ buf += cleanPath.substring(i);
1040
+ break;
1041
+ }
1042
+ segments.push(cleanPath.substring(i, end + 1));
1043
+ i = end;
1044
+ }
1045
+ else {
1046
+ buf += ch;
1047
+ }
1048
+ }
1049
+ if (buf) {
1050
+ segments.push(buf);
1051
+ }
1052
+ return segments;
1053
+ }
1054
+ function joinPathSegments(segments) {
1055
+ let result = '';
1056
+ for (const seg of segments) {
1057
+ if (seg.startsWith('[')) {
1058
+ result += seg;
1059
+ }
1060
+ else if (result) {
1061
+ result += '.' + seg;
1062
+ }
1063
+ else {
1064
+ result = seg;
1065
+ }
1066
+ }
1067
+ return result;
1068
+ }
454
1069
  function resolvePath(currentPath, targetPath) {
455
1070
  if (!targetPath.startsWith('.')) {
456
1071
  return targetPath;
@@ -458,39 +1073,33 @@ function resolvePath(currentPath, targetPath) {
458
1073
  const dotsMatch = targetPath.match(/^\.+/);
459
1074
  const dots = dotsMatch ? dotsMatch[0].length : 0;
460
1075
  const targetClean = targetPath.substring(dots);
461
- const cleanCurrentPath = currentPath.startsWith('.') ? currentPath.substring(1) : currentPath;
462
- const currentParts = cleanCurrentPath ? cleanCurrentPath.split('.') : [];
463
- const baseParts = currentParts.slice(0, currentParts.length - dots);
1076
+ const currentParts = tokenizePath(currentPath);
1077
+ const baseParts = currentParts.slice(0, Math.max(0, currentParts.length - dots));
464
1078
  if (targetClean) {
465
- baseParts.push(targetClean);
1079
+ baseParts.push(...tokenizePath(targetClean));
466
1080
  }
467
- return baseParts.join('.');
1081
+ return joinPathSegments(baseParts);
468
1082
  }
469
1083
  function getValueAtPath(obj, path) {
470
1084
  if (!obj || typeof obj !== 'object') {
471
1085
  return undefined;
472
1086
  }
473
- const cleanPath = path.startsWith('.') ? path.substring(1) : path;
474
- if (!cleanPath) {
1087
+ const parts = tokenizePath(path);
1088
+ if (parts.length === 0) {
475
1089
  return obj;
476
1090
  }
477
- const parts = cleanPath.split('.');
478
1091
  let current = obj;
479
1092
  for (const part of parts) {
480
1093
  if (current === null || current === undefined || typeof current !== 'object') {
481
1094
  return undefined;
482
1095
  }
483
- const matches = part.split('[');
484
- const baseKey = matches[0];
485
- current = current[baseKey];
486
- for (let i = 1; i < matches.length; i++) {
487
- if (current === null || current === undefined || typeof current !== 'object') {
488
- return undefined;
489
- }
490
- const idxStr = matches[i].replace(']', '');
491
- const idx = parseInt(idxStr, 10);
1096
+ if (part.startsWith('[') && part.endsWith(']')) {
1097
+ const idx = parseInt(part.slice(1, -1), 10);
492
1098
  current = current[idx];
493
1099
  }
1100
+ else {
1101
+ current = current[part];
1102
+ }
494
1103
  }
495
1104
  return current;
496
1105
  }
@@ -536,18 +1145,20 @@ export class MetadataStoreClass {
536
1145
  is(validator, value, options) {
537
1146
  const opt = options;
538
1147
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
539
- const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
1148
+ // Type predicates require the value already match T never coerce.
540
1149
  const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
541
- const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
1150
+ const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1151
+ const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
542
1152
  validator(value, '', ctx);
543
1153
  return ctx.success;
544
1154
  }
545
1155
  assert(validator, value, options) {
546
1156
  const opt = options;
547
1157
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
548
- const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
1158
+ const from = typeof opt === 'object' ? opt?.from : undefined;
549
1159
  const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
550
- const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
1160
+ const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1161
+ const ctx = { success: true, errors: [], mode, from, wrapArrays, mutate, root: value };
551
1162
  const res = validator(value, '', ctx);
552
1163
  if (!ctx.success) {
553
1164
  if (typeof opt === 'object' && opt?.errorFactory) {
@@ -557,26 +1168,48 @@ export class MetadataStoreClass {
557
1168
  }
558
1169
  return res;
559
1170
  }
1171
+ assertGuard(validator, value, options) {
1172
+ const opt = options;
1173
+ const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1174
+ // Assertion predicates require the value already match T — never coerce.
1175
+ const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1176
+ const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1177
+ const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
1178
+ validator(value, '', ctx);
1179
+ if (!ctx.success) {
1180
+ if (typeof opt === 'object' && opt?.errorFactory) {
1181
+ throw opt.errorFactory(ctx.errors);
1182
+ }
1183
+ throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
1184
+ }
1185
+ }
560
1186
  validate(validator, value, options) {
561
1187
  const opt = options;
562
1188
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
563
- const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
1189
+ const from = typeof opt === 'object' ? opt?.from : undefined;
564
1190
  const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
565
- const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
1191
+ const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1192
+ const ctx = { success: true, errors: [], mode, from, wrapArrays, mutate, root: value };
566
1193
  const res = validator(value, '', ctx);
567
1194
  return { success: ctx.success, errors: ctx.errors, data: res };
568
1195
  }
569
1196
  }
570
1197
  export function groupErrorsByPath(errors) {
571
1198
  const grouped = {};
572
- for (const err of errors) {
573
- if (!grouped[err.path]) {
574
- grouped[err.path] = { value: err.value, errors: [] };
575
- }
576
- if (!grouped[err.path].errors.includes(err.error)) {
577
- grouped[err.path].errors.push(err.error);
1199
+ const visit = (list) => {
1200
+ for (const err of list) {
1201
+ if (!grouped[err.path]) {
1202
+ grouped[err.path] = { value: err.value, errors: [] };
1203
+ }
1204
+ if (!grouped[err.path].errors.includes(err.error)) {
1205
+ grouped[err.path].errors.push(err.error);
1206
+ }
1207
+ if (err.issues?.length) {
1208
+ visit(err.issues);
1209
+ }
578
1210
  }
579
- }
1211
+ };
1212
+ visit(errors);
580
1213
  return grouped;
581
1214
  }
582
1215
  export const MetadataStore = new MetadataStoreClass();
@@ -608,6 +1241,67 @@ export function compileSchema(schema) {
608
1241
  compiledDefs.set(refPath, proxy);
609
1242
  return proxy;
610
1243
  }
1244
+ if (subSchema['x-typescript-type'] === 'Date') {
1245
+ return (v, path, ctx) => validators.date(v, path, ctx);
1246
+ }
1247
+ if (subSchema['x-typescript-type'] === 'RegExp') {
1248
+ return (v, path, ctx) => validators.regexp(v, path, ctx);
1249
+ }
1250
+ if (subSchema['x-typescript-type'] === 'bigint') {
1251
+ return (v, path, ctx) => validators.bigint(v, path, ctx);
1252
+ }
1253
+ if (subSchema['x-typescript-type'] === 'undefined') {
1254
+ return (v, path, ctx) => validators.undefined(v, path, ctx);
1255
+ }
1256
+ if (subSchema['x-typescript-type'] === 'Set') {
1257
+ const child = build(subSchema.items || {});
1258
+ return (v, path, ctx) => validators.set(v, path, ctx, child);
1259
+ }
1260
+ if (subSchema['x-typescript-type'] === 'Map') {
1261
+ const keyCheck = build(subSchema.key || { type: 'string' });
1262
+ const valueCheck = build(subSchema.value || {});
1263
+ return (v, path, ctx) => validators.map(v, path, ctx, keyCheck, valueCheck);
1264
+ }
1265
+ if (subSchema['x-typescript-type'] === 'Promise') {
1266
+ return (v, path, ctx) => {
1267
+ if (!(v instanceof Promise)) {
1268
+ report(ctx, path, 'Type<Promise>', v);
1269
+ }
1270
+ return v;
1271
+ };
1272
+ }
1273
+ if (typeof subSchema['x-typescript-type'] === 'string' &&
1274
+ ['Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'].includes(subSchema['x-typescript-type'])) {
1275
+ const typeName = subSchema['x-typescript-type'];
1276
+ return (v, path, ctx) => {
1277
+ const ctor = globalThis[typeName];
1278
+ if (!ctor || !(v instanceof ctor)) {
1279
+ report(ctx, path, `Type<${typeName}>`, v);
1280
+ }
1281
+ return v;
1282
+ };
1283
+ }
1284
+ if (subSchema.allOf) {
1285
+ const checks = subSchema.allOf.map((s) => build(s));
1286
+ return (v, path, ctx) => {
1287
+ const prevMode = ctx.mode;
1288
+ if (ctx.mode !== 'strip') {
1289
+ ctx.mode = 'relaxed';
1290
+ }
1291
+ let data = validators.objectShell(v, ctx);
1292
+ for (const check of checks) {
1293
+ const val = check(v, path, ctx);
1294
+ if (isPlainObject(val) && isPlainObject(data)) {
1295
+ Object.assign(data, val);
1296
+ }
1297
+ else {
1298
+ data = val;
1299
+ }
1300
+ }
1301
+ ctx.mode = prevMode;
1302
+ return data;
1303
+ };
1304
+ }
611
1305
  if (subSchema.type === 'string') {
612
1306
  const minLength = subSchema.minLength;
613
1307
  const maxLength = subSchema.maxLength;
@@ -629,7 +1323,7 @@ export function compileSchema(schema) {
629
1323
  validators.pattern(v, path, ctx, pattern, patternStr);
630
1324
  }
631
1325
  if (format !== undefined) {
632
- validators.format(v, path, ctx, format);
1326
+ v = validators.format(v, path, ctx, format);
633
1327
  }
634
1328
  return v;
635
1329
  };
@@ -638,6 +1332,8 @@ export function compileSchema(schema) {
638
1332
  const isInt = subSchema.type === 'integer';
639
1333
  const minimum = subSchema.minimum;
640
1334
  const maximum = subSchema.maximum;
1335
+ const exclusiveMinimum = subSchema.exclusiveMinimum;
1336
+ const exclusiveMaximum = subSchema.exclusiveMaximum;
641
1337
  const multipleOf = subSchema.multipleOf;
642
1338
  return (v, path, ctx) => {
643
1339
  v = validators.number(v, path, ctx);
@@ -653,6 +1349,12 @@ export function compileSchema(schema) {
653
1349
  if (maximum !== undefined) {
654
1350
  validators.maximum(v, path, ctx, maximum);
655
1351
  }
1352
+ if (exclusiveMinimum !== undefined) {
1353
+ validators.exclusiveMinimum(v, path, ctx, exclusiveMinimum);
1354
+ }
1355
+ if (exclusiveMaximum !== undefined) {
1356
+ validators.exclusiveMaximum(v, path, ctx, exclusiveMaximum);
1357
+ }
656
1358
  if (multipleOf !== undefined) {
657
1359
  validators.multipleOf(v, path, ctx, multipleOf);
658
1360
  }
@@ -704,31 +1406,25 @@ export function compileSchema(schema) {
704
1406
  const check = build(s);
705
1407
  return [key, isOptional, check];
706
1408
  });
707
- const allowedKeys = Object.keys(subSchema.properties || {});
1409
+ const knownKeys = Object.keys(subSchema.properties || {});
1410
+ const additional = 'additionalProperties' in subSchema
1411
+ ? subSchema.additionalProperties
1412
+ : false;
1413
+ const strictKeys = additional === false ? knownKeys : undefined;
1414
+ const additionalCheck = additional && typeof additional === 'object' ? build(additional) : undefined;
708
1415
  return (v, path, ctx) => {
709
- if (!validators.object(v, path, ctx, allowedKeys, 'Object')) {
1416
+ const obj = validators.object(v, path, ctx, strictKeys, 'Object');
1417
+ if (obj === false) {
710
1418
  return v;
711
1419
  }
712
- let data = v;
713
- if (ctx.mode === 'strip') {
714
- let hasAdditional = false;
715
- const keys = Object.keys(v);
716
- if (keys.length > allowedKeys.length) {
717
- hasAdditional = true;
718
- }
719
- else {
720
- for (let i = 0; i < keys.length; i++) {
721
- if (!allowedKeys.includes(keys[i])) {
722
- hasAdditional = true;
723
- break;
724
- }
725
- }
726
- }
727
- if (hasAdditional) {
728
- data = {};
729
- }
1420
+ const data = validators.objectShell(obj, ctx);
1421
+ validators.props(obj, data, path, ctx, propVals);
1422
+ if (additionalCheck) {
1423
+ validators.additionalProps(obj, data, path, ctx, knownKeys, additionalCheck);
1424
+ }
1425
+ else if (additional === false) {
1426
+ validators.stripExtras(data, ctx, knownKeys);
730
1427
  }
731
- validators.props(v, data, path, ctx, propVals);
732
1428
  return data;
733
1429
  };
734
1430
  }
@@ -746,24 +1442,31 @@ export function compileSchema(schema) {
746
1442
  return build(schema);
747
1443
  }
748
1444
  export function toZodIssues(errors) {
749
- return errors.map((err) => {
750
- const zodPath = err.path
751
- .split(/\.|\[|\]/)
752
- .filter(Boolean)
753
- .map((segment) => {
754
- if (isNaN(Number(segment))) {
755
- return segment;
756
- }
757
- return Number(segment);
758
- });
759
- const issue = {
760
- code: 'custom',
761
- path: zodPath,
762
- message: err.error,
763
- received: err.value
764
- };
765
- return issue;
766
- });
1445
+ const issues = [];
1446
+ const visit = (list) => {
1447
+ for (const err of list) {
1448
+ const zodPath = err.path
1449
+ .split(/\.|\[|\]/)
1450
+ .filter(Boolean)
1451
+ .map((segment) => {
1452
+ if (isNaN(Number(segment))) {
1453
+ return segment;
1454
+ }
1455
+ return Number(segment);
1456
+ });
1457
+ issues.push({
1458
+ code: 'custom',
1459
+ path: zodPath,
1460
+ message: err.error,
1461
+ received: err.value
1462
+ });
1463
+ if (err.issues?.length) {
1464
+ visit(err.issues);
1465
+ }
1466
+ }
1467
+ };
1468
+ visit(errors);
1469
+ return issues;
767
1470
  }
768
1471
  export class ZodLikeError extends Error {
769
1472
  issues;