@webergency-utils/typechecker 0.1.10 → 0.2.2

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