@webergency-utils/typechecker 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/engine/generators.js +6 -5
- package/dist/engine/hoister.js +23 -17
- package/dist/engine/resolver.d.ts +1 -1
- package/dist/engine/resolver.js +16 -9
- package/dist/runtime/casing.js +18 -1
- package/dist/runtime/validators.d.ts +8 -5
- package/dist/runtime/validators.js +733 -76
- package/dist/transformer.js +77 -24
- package/package.json +2 -1
|
@@ -18,6 +18,140 @@ function isPlainObject(v) {
|
|
|
18
18
|
const proto = Object.getPrototypeOf(v);
|
|
19
19
|
return proto === Object.prototype || proto === null;
|
|
20
20
|
}
|
|
21
|
+
function setOwnProperty(target, key, value) {
|
|
22
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
23
|
+
target[key] = value;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(target, key, {
|
|
27
|
+
value,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function assignOwnProperties(target, source) {
|
|
34
|
+
for (const key of Object.keys(source)) {
|
|
35
|
+
setOwnProperty(target, key, source[key]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function keySetHas(keys, key) {
|
|
39
|
+
return keys instanceof Set ? keys.has(key) : keys.includes(key);
|
|
40
|
+
}
|
|
41
|
+
function commitContainer(target, source) {
|
|
42
|
+
if (target === source) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
46
|
+
target.length = source.length;
|
|
47
|
+
for (let i = 0; i < source.length; i++) {
|
|
48
|
+
target[i] = source[i];
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (target instanceof Set && source instanceof Set) {
|
|
53
|
+
target.clear();
|
|
54
|
+
for (const value of source) {
|
|
55
|
+
target.add(value);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (target instanceof Map && source instanceof Map) {
|
|
60
|
+
target.clear();
|
|
61
|
+
for (const [key, value] of source) {
|
|
62
|
+
target.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
67
|
+
for (const key of Object.keys(target)) {
|
|
68
|
+
if (!Object.hasOwn(source, key)) {
|
|
69
|
+
delete target[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const key of Object.keys(source)) {
|
|
73
|
+
if (!Object.hasOwn(target, key) || target[key] !== source[key]) {
|
|
74
|
+
setOwnProperty(target, key, source[key]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const regexSafetyCache = new WeakMap();
|
|
82
|
+
function isSafeRegexSource(source) {
|
|
83
|
+
if (source.length > 1024 || /\\[1-9]/.test(source)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const groups = [];
|
|
87
|
+
let inClass = false;
|
|
88
|
+
let escaped = false;
|
|
89
|
+
for (let i = 0; i < source.length; i++) {
|
|
90
|
+
const ch = source[i];
|
|
91
|
+
if (escaped) {
|
|
92
|
+
escaped = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === '\\') {
|
|
96
|
+
escaped = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === '[') {
|
|
100
|
+
inClass = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === ']' && inClass) {
|
|
104
|
+
inClass = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (inClass) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (ch === '(') {
|
|
111
|
+
groups.push({ hasRepeat: false, hasAlternation: false });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (ch === '|' && groups.length > 0) {
|
|
115
|
+
groups[groups.length - 1].hasAlternation = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ch === '*' || ch === '+' || ch === '{') {
|
|
119
|
+
if (groups.length > 0) {
|
|
120
|
+
groups[groups.length - 1].hasRepeat = true;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === ')' && groups.length > 0) {
|
|
125
|
+
const group = groups.pop();
|
|
126
|
+
const next = source[i + 1];
|
|
127
|
+
const isRepeated = next === '*' || next === '+' || next === '{';
|
|
128
|
+
if (isRepeated && (group.hasRepeat || group.hasAlternation)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (isRepeated && groups.length > 0) {
|
|
132
|
+
groups[groups.length - 1].hasRepeat = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
function isRegexSafe(regex) {
|
|
139
|
+
const cached = regexSafetyCache.get(regex);
|
|
140
|
+
if (cached !== undefined) {
|
|
141
|
+
return cached;
|
|
142
|
+
}
|
|
143
|
+
const safe = isSafeRegexSource(regex.source);
|
|
144
|
+
regexSafetyCache.set(regex, safe);
|
|
145
|
+
return safe;
|
|
146
|
+
}
|
|
147
|
+
function createSafeRegex(source, flags) {
|
|
148
|
+
if (!isSafeRegexSource(source)) {
|
|
149
|
+
throw new Error(`Unsafe regular expression: ${source}`);
|
|
150
|
+
}
|
|
151
|
+
const regex = flags === undefined ? new RegExp(source) : new RegExp(source, flags);
|
|
152
|
+
regexSafetyCache.set(regex, true);
|
|
153
|
+
return regex;
|
|
154
|
+
}
|
|
21
155
|
function testRegex(regex, value) {
|
|
22
156
|
if (!regex.global && !regex.sticky) {
|
|
23
157
|
return regex.test(value);
|
|
@@ -83,8 +217,12 @@ export function coerceQueryNumber(v) {
|
|
|
83
217
|
return v;
|
|
84
218
|
}
|
|
85
219
|
if (typeof v === 'string' && v.trim() !== '') {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
220
|
+
const normalized = v.trim();
|
|
221
|
+
if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
|
|
222
|
+
return v;
|
|
223
|
+
}
|
|
224
|
+
const parsed = Number(normalized);
|
|
225
|
+
if (Number.isFinite(parsed)) {
|
|
88
226
|
return parsed;
|
|
89
227
|
}
|
|
90
228
|
}
|
|
@@ -289,17 +427,32 @@ function isUriTemplate(value) {
|
|
|
289
427
|
return URI_TEMPLATE_RE.test(value);
|
|
290
428
|
}
|
|
291
429
|
function parseFormatDate(value) {
|
|
292
|
-
if (typeof value !== 'string'
|
|
430
|
+
if (typeof value !== 'string') {
|
|
293
431
|
return undefined;
|
|
294
432
|
}
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
433
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
434
|
+
if (!match) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
const year = Number(match[1]);
|
|
438
|
+
const month = Number(match[2]);
|
|
439
|
+
const day = Number(match[3]);
|
|
440
|
+
const parsed = new Date(0);
|
|
441
|
+
parsed.setUTCHours(0, 0, 0, 0);
|
|
442
|
+
parsed.setUTCFullYear(year, month - 1, day);
|
|
443
|
+
if (parsed.getUTCFullYear() !== year ||
|
|
444
|
+
parsed.getUTCMonth() !== month - 1 ||
|
|
445
|
+
parsed.getUTCDate() !== day) {
|
|
297
446
|
return undefined;
|
|
298
447
|
}
|
|
299
448
|
return parsed;
|
|
300
449
|
}
|
|
301
450
|
function parseFormatDateTime(value) {
|
|
302
|
-
if (typeof value !== 'string'
|
|
451
|
+
if (typeof value !== 'string' ||
|
|
452
|
+
!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/i.test(value)) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
if (!parseFormatDate(value.slice(0, 10))) {
|
|
303
456
|
return undefined;
|
|
304
457
|
}
|
|
305
458
|
const parsed = new Date(value);
|
|
@@ -308,30 +461,236 @@ function parseFormatDateTime(value) {
|
|
|
308
461
|
}
|
|
309
462
|
return parsed;
|
|
310
463
|
}
|
|
311
|
-
function
|
|
312
|
-
if (
|
|
313
|
-
return
|
|
464
|
+
function deepEqual(left, right, seen = new WeakMap()) {
|
|
465
|
+
if (left === right) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
const seenRight = seen.get(left);
|
|
472
|
+
if (seenRight?.has(right)) {
|
|
473
|
+
return true;
|
|
314
474
|
}
|
|
315
|
-
if (
|
|
475
|
+
if (seenRight) {
|
|
476
|
+
seenRight.add(right);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
seen.set(left, new WeakSet([right]));
|
|
480
|
+
}
|
|
481
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
482
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
for (let i = 0; i < left.length; i++) {
|
|
486
|
+
if (!deepEqual(left[i], right[i], seen)) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (left instanceof Date || right instanceof Date) {
|
|
493
|
+
return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
|
|
494
|
+
}
|
|
495
|
+
if (left instanceof RegExp || right instanceof RegExp) {
|
|
496
|
+
return left instanceof RegExp &&
|
|
497
|
+
right instanceof RegExp &&
|
|
498
|
+
left.source === right.source &&
|
|
499
|
+
left.flags === right.flags;
|
|
500
|
+
}
|
|
501
|
+
if (left instanceof Set || right instanceof Set) {
|
|
502
|
+
if (!(left instanceof Set) || !(right instanceof Set) || left.size !== right.size) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
const unmatched = [...right];
|
|
506
|
+
for (const value of left) {
|
|
507
|
+
const index = unmatched.findIndex(candidate => deepEqual(value, candidate));
|
|
508
|
+
if (index === -1) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
unmatched.splice(index, 1);
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
if (left instanceof Map || right instanceof Map) {
|
|
516
|
+
if (!(left instanceof Map) || !(right instanceof Map) || left.size !== right.size) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const unmatched = [...right.entries()];
|
|
520
|
+
for (const [key, value] of left) {
|
|
521
|
+
const index = unmatched.findIndex(([candidateKey, candidateValue]) => deepEqual(key, candidateKey) && deepEqual(value, candidateValue));
|
|
522
|
+
if (index === -1) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
unmatched.splice(index, 1);
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
const leftKeys = Object.keys(left).sort();
|
|
530
|
+
const rightKeys = Object.keys(right).sort();
|
|
531
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
for (let i = 0; i < leftKeys.length; i++) {
|
|
535
|
+
const key = leftKeys[i];
|
|
536
|
+
if (key !== rightKeys[i] || !deepEqual(left[key], right[key], seen)) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
function mixHash(h, part) {
|
|
543
|
+
return Math.imul(h ^ (part >>> 0), 16777619);
|
|
544
|
+
}
|
|
545
|
+
function mixStringHash(h, value) {
|
|
546
|
+
const len = value.length;
|
|
547
|
+
h = mixHash(h, len);
|
|
548
|
+
// Fast paths for short property names / small strings.
|
|
549
|
+
if (len === 1) {
|
|
550
|
+
return mixHash(h, value.charCodeAt(0));
|
|
551
|
+
}
|
|
552
|
+
if (len === 2) {
|
|
553
|
+
return mixHash(mixHash(h, value.charCodeAt(0)), value.charCodeAt(1));
|
|
554
|
+
}
|
|
555
|
+
if (len === 3) {
|
|
556
|
+
h = mixHash(h, value.charCodeAt(0));
|
|
557
|
+
h = mixHash(h, value.charCodeAt(1));
|
|
558
|
+
return mixHash(h, value.charCodeAt(2));
|
|
559
|
+
}
|
|
560
|
+
for (let i = 0; i < len; i++) {
|
|
561
|
+
h = mixHash(h, value.charCodeAt(i));
|
|
562
|
+
}
|
|
563
|
+
return h;
|
|
564
|
+
}
|
|
565
|
+
const uniqueFloat64Buf = new ArrayBuffer(8);
|
|
566
|
+
const uniqueFloat64View = new Float64Array(uniqueFloat64Buf);
|
|
567
|
+
const uniqueFloat64Words = new Int32Array(uniqueFloat64Buf);
|
|
568
|
+
function mixNumberHash(h, value) {
|
|
569
|
+
if (Object.is(value, -0)) {
|
|
570
|
+
return mixHash(h, 0x30000001);
|
|
571
|
+
}
|
|
572
|
+
if (Number.isNaN(value)) {
|
|
573
|
+
return mixHash(h, 0x30000002);
|
|
574
|
+
}
|
|
575
|
+
if (value === Infinity) {
|
|
576
|
+
return mixHash(h, 0x30000003);
|
|
577
|
+
}
|
|
578
|
+
if (value === -Infinity) {
|
|
579
|
+
return mixHash(h, 0x30000004);
|
|
580
|
+
}
|
|
581
|
+
uniqueFloat64View[0] = value;
|
|
582
|
+
return mixHash(mixHash(h, uniqueFloat64Words[0]), uniqueFloat64Words[1]);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Order-independent content hash for plain objects / arrays.
|
|
586
|
+
* Returns `undefined` for cycles and non-plain values (caller uses deepEqual list).
|
|
587
|
+
* Collisions are resolved with `deepEqual`.
|
|
588
|
+
*/
|
|
589
|
+
function uniqueContentHash(value, seen) {
|
|
590
|
+
if (value === null) {
|
|
591
|
+
return 0x10000001;
|
|
592
|
+
}
|
|
593
|
+
if (value === undefined) {
|
|
594
|
+
return 0x10000002;
|
|
595
|
+
}
|
|
596
|
+
const type = typeof value;
|
|
597
|
+
if (type === 'string') {
|
|
598
|
+
return mixStringHash(0x20000000, value) >>> 0;
|
|
599
|
+
}
|
|
600
|
+
if (type === 'number') {
|
|
601
|
+
return mixNumberHash(0x30000000, value) >>> 0;
|
|
602
|
+
}
|
|
603
|
+
if (type === 'boolean') {
|
|
604
|
+
return value ? 0x40000001 : 0x40000002;
|
|
605
|
+
}
|
|
606
|
+
if (type === 'bigint') {
|
|
607
|
+
// Split into 32-bit limbs — avoids string alloc for common small bigints.
|
|
608
|
+
let h = 0x50000000;
|
|
609
|
+
let n = value < 0n ? -value : value;
|
|
610
|
+
if (value < 0n) {
|
|
611
|
+
h = mixHash(h, 1);
|
|
612
|
+
}
|
|
613
|
+
while (n > 0n) {
|
|
614
|
+
h = mixHash(h, Number(n & 0xffffffffn));
|
|
615
|
+
n >>= 32n;
|
|
616
|
+
}
|
|
617
|
+
return h >>> 0;
|
|
618
|
+
}
|
|
619
|
+
if (type !== 'object') {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
if (value instanceof Date) {
|
|
623
|
+
return mixNumberHash(0x60000000, value.getTime()) >>> 0;
|
|
624
|
+
}
|
|
625
|
+
if (value instanceof RegExp) {
|
|
626
|
+
return mixStringHash(0x70000000, `${value.source}/${value.flags}`) >>> 0;
|
|
627
|
+
}
|
|
628
|
+
if (value instanceof Map || value instanceof Set || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
if (seen?.has(value)) {
|
|
316
632
|
return undefined;
|
|
317
633
|
}
|
|
318
|
-
seen.add(value);
|
|
319
634
|
if (Array.isArray(value)) {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
635
|
+
const cycleSet = seen ?? new WeakSet();
|
|
636
|
+
cycleSet.add(value);
|
|
637
|
+
let h = mixHash(0x80000000, value.length);
|
|
638
|
+
for (let i = 0; i < value.length; i++) {
|
|
639
|
+
const child = uniqueContentHash(value[i], cycleSet);
|
|
640
|
+
if (child === undefined) {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
h = mixHash(h, child);
|
|
644
|
+
}
|
|
645
|
+
return h >>> 0;
|
|
646
|
+
}
|
|
647
|
+
const proto = Object.getPrototypeOf(value);
|
|
648
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
const keys = Object.keys(value);
|
|
652
|
+
const keyCount = keys.length;
|
|
653
|
+
if (keyCount > 1) {
|
|
654
|
+
// Avoid Array#sort when already ordered (common for same-shape rows).
|
|
655
|
+
let ordered = true;
|
|
656
|
+
for (let i = 1; i < keyCount; i++) {
|
|
657
|
+
if (keys[i] < keys[i - 1]) {
|
|
658
|
+
ordered = false;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!ordered) {
|
|
663
|
+
keys.sort();
|
|
664
|
+
}
|
|
325
665
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
666
|
+
let cycleSet = seen;
|
|
667
|
+
let h = mixHash(0x90000000, keyCount);
|
|
668
|
+
for (let i = 0; i < keyCount; i++) {
|
|
669
|
+
const key = keys[i];
|
|
670
|
+
const childValue = value[key];
|
|
671
|
+
h = mixStringHash(h, key);
|
|
672
|
+
if (childValue !== null && typeof childValue === 'object') {
|
|
673
|
+
cycleSet ??= new WeakSet();
|
|
674
|
+
cycleSet.add(value);
|
|
675
|
+
}
|
|
676
|
+
const child = uniqueContentHash(childValue, cycleSet);
|
|
677
|
+
if (child === undefined) {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
h = mixHash(h, child);
|
|
681
|
+
}
|
|
682
|
+
return h >>> 0;
|
|
329
683
|
}
|
|
330
684
|
export const validators = {
|
|
331
685
|
coerceQueryNumber,
|
|
332
686
|
coerceQueryBoolean,
|
|
333
687
|
coerceQueryDate,
|
|
334
688
|
coerceJsonDate,
|
|
689
|
+
safeRegExp: createSafeRegex,
|
|
690
|
+
assign: (target, source) => {
|
|
691
|
+
assignOwnProperties(target, source);
|
|
692
|
+
return target;
|
|
693
|
+
},
|
|
335
694
|
string: (v, path, ctx) => {
|
|
336
695
|
if (typeof v === 'string') {
|
|
337
696
|
return v;
|
|
@@ -547,11 +906,15 @@ export const validators = {
|
|
|
547
906
|
return v;
|
|
548
907
|
}
|
|
549
908
|
}
|
|
550
|
-
|
|
551
|
-
|
|
909
|
+
if (shouldMutate(ctx)) {
|
|
910
|
+
for (let i = 0; i < v.length; i++) {
|
|
911
|
+
v[i] = childValidator(v[i], path + '[' + i + ']', ctx);
|
|
912
|
+
}
|
|
913
|
+
return v;
|
|
914
|
+
}
|
|
915
|
+
const data = [];
|
|
552
916
|
for (let i = 0; i < v.length; i++) {
|
|
553
|
-
|
|
554
|
-
data[i] = val;
|
|
917
|
+
data[i] = childValidator(v[i], path + '[' + i + ']', ctx);
|
|
555
918
|
}
|
|
556
919
|
return data;
|
|
557
920
|
},
|
|
@@ -562,7 +925,7 @@ export const validators = {
|
|
|
562
925
|
const wasSuccess = ctx.success;
|
|
563
926
|
const result = validator(val, path + '.' + key, ctx);
|
|
564
927
|
if (ctx.success) {
|
|
565
|
-
data
|
|
928
|
+
setOwnProperty(data, key, result);
|
|
566
929
|
}
|
|
567
930
|
else if (isOptional && val === undefined) {
|
|
568
931
|
ctx.errors.length = oldErrors;
|
|
@@ -587,7 +950,7 @@ export const validators = {
|
|
|
587
950
|
return data;
|
|
588
951
|
}
|
|
589
952
|
for (const k of Object.keys(data)) {
|
|
590
|
-
if (!allowedKeys
|
|
953
|
+
if (!keySetHas(allowedKeys, k)) {
|
|
591
954
|
delete data[k];
|
|
592
955
|
}
|
|
593
956
|
}
|
|
@@ -598,10 +961,10 @@ export const validators = {
|
|
|
598
961
|
return;
|
|
599
962
|
}
|
|
600
963
|
for (const key of Object.keys(v)) {
|
|
601
|
-
if (knownKeys
|
|
964
|
+
if (keySetHas(knownKeys, key)) {
|
|
602
965
|
continue;
|
|
603
966
|
}
|
|
604
|
-
data
|
|
967
|
+
setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
|
|
605
968
|
}
|
|
606
969
|
},
|
|
607
970
|
object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
|
|
@@ -623,7 +986,7 @@ export const validators = {
|
|
|
623
986
|
}
|
|
624
987
|
if (ctx.mode === 'strict' && allowedKeys) {
|
|
625
988
|
for (const k of Object.keys(v)) {
|
|
626
|
-
if (!allowedKeys
|
|
989
|
+
if (!keySetHas(allowedKeys, k)) {
|
|
627
990
|
report(ctx, path, `PropertyNotAllowed<${k}>`, v[k]);
|
|
628
991
|
}
|
|
629
992
|
}
|
|
@@ -674,7 +1037,13 @@ export const validators = {
|
|
|
674
1037
|
},
|
|
675
1038
|
multipleOf: (v, path, ctx, n, message) => {
|
|
676
1039
|
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
677
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
const divisor = BigInt(n);
|
|
1042
|
+
if (divisor === 0n || BigInt(v) % divisor !== 0n) {
|
|
1043
|
+
report(ctx, path, `MultipleOf<${n}>`, v, message);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
678
1047
|
report(ctx, path, `MultipleOf<${n}>`, v, message);
|
|
679
1048
|
}
|
|
680
1049
|
}
|
|
@@ -684,6 +1053,10 @@ export const validators = {
|
|
|
684
1053
|
return v;
|
|
685
1054
|
},
|
|
686
1055
|
pattern: (v, path, ctx, regex, expected, message) => {
|
|
1056
|
+
if (!isRegexSafe(regex)) {
|
|
1057
|
+
report(ctx, path, 'UnsafePattern', v, message);
|
|
1058
|
+
return v;
|
|
1059
|
+
}
|
|
687
1060
|
if (!testRegex(regex, v)) {
|
|
688
1061
|
report(ctx, path, expected, v, message);
|
|
689
1062
|
}
|
|
@@ -740,7 +1113,7 @@ export const validators = {
|
|
|
740
1113
|
case 'password': break; // Anything is a password
|
|
741
1114
|
case 'regex':
|
|
742
1115
|
try {
|
|
743
|
-
|
|
1116
|
+
createSafeRegex(v);
|
|
744
1117
|
}
|
|
745
1118
|
catch {
|
|
746
1119
|
isValid = false;
|
|
@@ -802,17 +1175,147 @@ export const validators = {
|
|
|
802
1175
|
return v;
|
|
803
1176
|
},
|
|
804
1177
|
uniqueItems: (v, path, ctx, message) => {
|
|
805
|
-
|
|
1178
|
+
// Typed scalar sets avoid per-item string encoding (SameValueZero for numbers,
|
|
1179
|
+
// with an explicit -0 flag because Set collapses -0 with 0).
|
|
1180
|
+
let seenStrings;
|
|
1181
|
+
let seenNumbers;
|
|
1182
|
+
let seenBigints;
|
|
1183
|
+
let seenDates;
|
|
1184
|
+
let seenRegex;
|
|
1185
|
+
let seenNull = false;
|
|
1186
|
+
let seenUndefined = false;
|
|
1187
|
+
let seenTrue = false;
|
|
1188
|
+
let seenFalse = false;
|
|
1189
|
+
let seenNegZero = false;
|
|
1190
|
+
// First item per hash; collision buckets allocated only when a hash repeats.
|
|
1191
|
+
const firstByHash = new Map();
|
|
1192
|
+
const collisionBuckets = new Map();
|
|
1193
|
+
const complex = [];
|
|
806
1194
|
for (let i = 0; i < v.length; i++) {
|
|
807
1195
|
const item = v[i];
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1196
|
+
if (item === null) {
|
|
1197
|
+
if (seenNull) {
|
|
1198
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1199
|
+
return v;
|
|
1200
|
+
}
|
|
1201
|
+
seenNull = true;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (item === undefined) {
|
|
1205
|
+
if (seenUndefined) {
|
|
1206
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1207
|
+
return v;
|
|
1208
|
+
}
|
|
1209
|
+
seenUndefined = true;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const type = typeof item;
|
|
1213
|
+
if (type === 'string') {
|
|
1214
|
+
seenStrings ??= new Set();
|
|
1215
|
+
if (seenStrings.has(item)) {
|
|
1216
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1217
|
+
return v;
|
|
1218
|
+
}
|
|
1219
|
+
seenStrings.add(item);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (type === 'number') {
|
|
1223
|
+
if (Object.is(item, -0)) {
|
|
1224
|
+
if (seenNegZero) {
|
|
1225
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1226
|
+
return v;
|
|
1227
|
+
}
|
|
1228
|
+
seenNegZero = true;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
seenNumbers ??= new Set();
|
|
1232
|
+
if (seenNumbers.has(item)) {
|
|
1233
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1234
|
+
return v;
|
|
1235
|
+
}
|
|
1236
|
+
seenNumbers.add(item);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
if (type === 'boolean') {
|
|
1240
|
+
if (item) {
|
|
1241
|
+
if (seenTrue) {
|
|
1242
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1243
|
+
return v;
|
|
1244
|
+
}
|
|
1245
|
+
seenTrue = true;
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (seenFalse) {
|
|
1249
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1250
|
+
return v;
|
|
1251
|
+
}
|
|
1252
|
+
seenFalse = true;
|
|
1253
|
+
}
|
|
1254
|
+
continue;
|
|
814
1255
|
}
|
|
815
|
-
|
|
1256
|
+
if (type === 'bigint') {
|
|
1257
|
+
seenBigints ??= new Set();
|
|
1258
|
+
if (seenBigints.has(item)) {
|
|
1259
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1260
|
+
return v;
|
|
1261
|
+
}
|
|
1262
|
+
seenBigints.add(item);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (type === 'object') {
|
|
1266
|
+
if (item instanceof Date) {
|
|
1267
|
+
const time = item.getTime();
|
|
1268
|
+
seenDates ??= new Set();
|
|
1269
|
+
if (seenDates.has(time)) {
|
|
1270
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1271
|
+
return v;
|
|
1272
|
+
}
|
|
1273
|
+
seenDates.add(time);
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (item instanceof RegExp) {
|
|
1277
|
+
const key = `${item.source}/${item.flags}`;
|
|
1278
|
+
seenRegex ??= new Set();
|
|
1279
|
+
if (seenRegex.has(key)) {
|
|
1280
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1281
|
+
return v;
|
|
1282
|
+
}
|
|
1283
|
+
seenRegex.add(key);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const hash = uniqueContentHash(item);
|
|
1287
|
+
if (hash !== undefined) {
|
|
1288
|
+
const first = firstByHash.get(hash);
|
|
1289
|
+
if (first === undefined) {
|
|
1290
|
+
firstByHash.set(hash, item);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
let bucket = collisionBuckets.get(hash);
|
|
1294
|
+
if (!bucket) {
|
|
1295
|
+
if (deepEqual(item, first)) {
|
|
1296
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1297
|
+
return v;
|
|
1298
|
+
}
|
|
1299
|
+
collisionBuckets.set(hash, [first, item]);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
for (let j = 0; j < bucket.length; j++) {
|
|
1303
|
+
if (deepEqual(item, bucket[j])) {
|
|
1304
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1305
|
+
return v;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
bucket.push(item);
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (let j = 0; j < complex.length; j++) {
|
|
1313
|
+
if (deepEqual(item, complex[j])) {
|
|
1314
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
1315
|
+
return v;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
complex.push(item);
|
|
816
1319
|
}
|
|
817
1320
|
return v;
|
|
818
1321
|
},
|
|
@@ -824,12 +1327,29 @@ export const validators = {
|
|
|
824
1327
|
},
|
|
825
1328
|
union: (v, path, ctx, checks, expected = 'Type<Union>') => {
|
|
826
1329
|
const unionErrors = [];
|
|
1330
|
+
// Speculative arms always use a side tree so a failed arm cannot poison the next.
|
|
1331
|
+
const subCtx = {
|
|
1332
|
+
success: true,
|
|
1333
|
+
errors: [],
|
|
1334
|
+
mode: ctx.mode,
|
|
1335
|
+
from: undefined,
|
|
1336
|
+
mutate: false,
|
|
1337
|
+
root: ctx.root
|
|
1338
|
+
};
|
|
1339
|
+
const accept = (val) => {
|
|
1340
|
+
if (shouldMutate(ctx) && commitContainer(v, val)) {
|
|
1341
|
+
return v;
|
|
1342
|
+
}
|
|
1343
|
+
return val;
|
|
1344
|
+
};
|
|
827
1345
|
// Pass 1: No conversion
|
|
828
1346
|
for (const check of checks) {
|
|
829
|
-
|
|
1347
|
+
subCtx.success = true;
|
|
1348
|
+
subCtx.errors.length = 0;
|
|
1349
|
+
subCtx.from = undefined;
|
|
830
1350
|
const val = check(v, path, subCtx);
|
|
831
1351
|
if (subCtx.success) {
|
|
832
|
-
return val;
|
|
1352
|
+
return accept(val);
|
|
833
1353
|
}
|
|
834
1354
|
unionErrors.push(...subCtx.errors);
|
|
835
1355
|
}
|
|
@@ -837,10 +1357,12 @@ export const validators = {
|
|
|
837
1357
|
if (ctx.from) {
|
|
838
1358
|
unionErrors.length = 0;
|
|
839
1359
|
for (const check of checks) {
|
|
840
|
-
|
|
1360
|
+
subCtx.success = true;
|
|
1361
|
+
subCtx.errors.length = 0;
|
|
1362
|
+
subCtx.from = ctx.from;
|
|
841
1363
|
const val = check(v, path, subCtx);
|
|
842
1364
|
if (subCtx.success) {
|
|
843
|
-
return val;
|
|
1365
|
+
return accept(val);
|
|
844
1366
|
}
|
|
845
1367
|
unionErrors.push(...subCtx.errors);
|
|
846
1368
|
}
|
|
@@ -871,8 +1393,13 @@ export const validators = {
|
|
|
871
1393
|
return v;
|
|
872
1394
|
}
|
|
873
1395
|
}
|
|
874
|
-
|
|
875
|
-
|
|
1396
|
+
if (shouldMutate(ctx)) {
|
|
1397
|
+
for (let i = 0; i < checks.length; i++) {
|
|
1398
|
+
v[i] = checks[i](v[i], path + '[' + i + ']', ctx);
|
|
1399
|
+
}
|
|
1400
|
+
return v;
|
|
1401
|
+
}
|
|
1402
|
+
const data = [];
|
|
876
1403
|
for (let i = 0; i < checks.length; i++) {
|
|
877
1404
|
data[i] = checks[i](v[i], path + '[' + i + ']', ctx);
|
|
878
1405
|
}
|
|
@@ -945,7 +1472,7 @@ export const validators = {
|
|
|
945
1472
|
const mutate = shouldMutate(ctx);
|
|
946
1473
|
const data = mutate ? v : {};
|
|
947
1474
|
for (const key of Object.keys(v)) {
|
|
948
|
-
data
|
|
1475
|
+
setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
|
|
949
1476
|
}
|
|
950
1477
|
return data;
|
|
951
1478
|
},
|
|
@@ -972,13 +1499,17 @@ export const validators = {
|
|
|
972
1499
|
return v;
|
|
973
1500
|
}
|
|
974
1501
|
}
|
|
975
|
-
const mutate = shouldMutate(ctx);
|
|
976
1502
|
const source = [...v];
|
|
977
|
-
|
|
1503
|
+
let index = 0;
|
|
1504
|
+
if (shouldMutate(ctx)) {
|
|
978
1505
|
v.clear();
|
|
1506
|
+
for (const item of source) {
|
|
1507
|
+
v.add(childValidator(item, `${path}[${index}]`, ctx));
|
|
1508
|
+
index++;
|
|
1509
|
+
}
|
|
1510
|
+
return v;
|
|
979
1511
|
}
|
|
980
|
-
const data =
|
|
981
|
-
let index = 0;
|
|
1512
|
+
const data = new Set();
|
|
982
1513
|
for (const item of source) {
|
|
983
1514
|
data.add(childValidator(item, `${path}[${index}]`, ctx));
|
|
984
1515
|
index++;
|
|
@@ -1005,12 +1536,17 @@ export const validators = {
|
|
|
1005
1536
|
return v;
|
|
1006
1537
|
}
|
|
1007
1538
|
}
|
|
1008
|
-
const mutate = shouldMutate(ctx);
|
|
1009
1539
|
const source = [...v.entries()];
|
|
1010
|
-
if (
|
|
1540
|
+
if (shouldMutate(ctx)) {
|
|
1011
1541
|
v.clear();
|
|
1542
|
+
for (const [key, val] of source) {
|
|
1543
|
+
const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
|
|
1544
|
+
const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
|
|
1545
|
+
v.set(validatedKey, validatedVal);
|
|
1546
|
+
}
|
|
1547
|
+
return v;
|
|
1012
1548
|
}
|
|
1013
|
-
const data =
|
|
1549
|
+
const data = new Map();
|
|
1014
1550
|
for (const [key, val] of source) {
|
|
1015
1551
|
const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
|
|
1016
1552
|
const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
|
|
@@ -1137,8 +1673,11 @@ export class MetadataStoreClass {
|
|
|
1137
1673
|
return schema;
|
|
1138
1674
|
}
|
|
1139
1675
|
getOrCompileSchema(schema) {
|
|
1676
|
+
if (typeof schema === 'boolean') {
|
|
1677
|
+
return compileSchema(schema);
|
|
1678
|
+
}
|
|
1140
1679
|
if (typeof schema !== 'object' || schema === null) {
|
|
1141
|
-
throw new Error('Invalid JSON Schema: must be a non-null object');
|
|
1680
|
+
throw new Error('Invalid JSON Schema: must be a non-null object or boolean');
|
|
1142
1681
|
}
|
|
1143
1682
|
let compiled = this.compiledSchemas.get(schema);
|
|
1144
1683
|
if (!compiled) {
|
|
@@ -1151,14 +1690,16 @@ export class MetadataStoreClass {
|
|
|
1151
1690
|
const opt = options;
|
|
1152
1691
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1153
1692
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1154
|
-
// Always mutate — no returned data. Root must stay the same binding (res === value).
|
|
1155
1693
|
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1156
1694
|
const res = validator(value, '', ctx);
|
|
1157
1695
|
if (!ctx.success) {
|
|
1158
1696
|
return false;
|
|
1159
1697
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1698
|
+
if (res === value) {
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
// Fallback when a branch returned a side tree (e.g. union) that still needs copying.
|
|
1702
|
+
return commitContainer(value, res);
|
|
1162
1703
|
}
|
|
1163
1704
|
assert(validator, value, options) {
|
|
1164
1705
|
const opt = options;
|
|
@@ -1173,16 +1714,21 @@ export class MetadataStoreClass {
|
|
|
1173
1714
|
}
|
|
1174
1715
|
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
1175
1716
|
}
|
|
1717
|
+
if (mutate) {
|
|
1718
|
+
if (res === value || commitContainer(value, res)) {
|
|
1719
|
+
return value;
|
|
1720
|
+
}
|
|
1721
|
+
return res;
|
|
1722
|
+
}
|
|
1176
1723
|
return res;
|
|
1177
1724
|
}
|
|
1178
1725
|
assertGuard(validator, value, options) {
|
|
1179
1726
|
const opt = options;
|
|
1180
1727
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1181
1728
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1182
|
-
// Always mutate — no returned data. Root must stay the same binding (res === value).
|
|
1183
1729
|
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1184
1730
|
const res = validator(value, '', ctx);
|
|
1185
|
-
if (ctx.success && res === value) {
|
|
1731
|
+
if (ctx.success && (res === value || commitContainer(value, res))) {
|
|
1186
1732
|
return;
|
|
1187
1733
|
}
|
|
1188
1734
|
// Root was replaced (e.g. primitive coerce) — binding unchanged; report a normal type failure.
|
|
@@ -1191,6 +1737,9 @@ export class MetadataStoreClass {
|
|
|
1191
1737
|
ctx.errors.length = 0;
|
|
1192
1738
|
ctx.from = undefined;
|
|
1193
1739
|
validator(value, '', ctx);
|
|
1740
|
+
if (ctx.success) {
|
|
1741
|
+
report(ctx, '', 'RootNotRewritable', value);
|
|
1742
|
+
}
|
|
1194
1743
|
}
|
|
1195
1744
|
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
1196
1745
|
throw opt.errorFactory(ctx.errors);
|
|
@@ -1204,15 +1753,24 @@ export class MetadataStoreClass {
|
|
|
1204
1753
|
const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
|
|
1205
1754
|
const ctx = { success: true, errors: [], mode, from, mutate, root: value };
|
|
1206
1755
|
const res = validator(value, '', ctx);
|
|
1207
|
-
|
|
1756
|
+
if (!ctx.success) {
|
|
1757
|
+
return { success: false, errors: ctx.errors };
|
|
1758
|
+
}
|
|
1759
|
+
if (mutate) {
|
|
1760
|
+
if (res === value || commitContainer(value, res)) {
|
|
1761
|
+
return { success: true, errors: [], data: value };
|
|
1762
|
+
}
|
|
1763
|
+
return { success: true, errors: [], data: res };
|
|
1764
|
+
}
|
|
1765
|
+
return { success: true, errors: [], data: res };
|
|
1208
1766
|
}
|
|
1209
1767
|
}
|
|
1210
1768
|
export function groupErrorsByPath(errors) {
|
|
1211
|
-
const grouped =
|
|
1769
|
+
const grouped = Object.create(null);
|
|
1212
1770
|
const visit = (list) => {
|
|
1213
1771
|
for (const err of list) {
|
|
1214
|
-
if (!grouped
|
|
1215
|
-
grouped
|
|
1772
|
+
if (!Object.hasOwn(grouped, err.path)) {
|
|
1773
|
+
setOwnProperty(grouped, err.path, { value: err.value, errors: [] });
|
|
1216
1774
|
}
|
|
1217
1775
|
if (!grouped[err.path].errors.includes(err.error)) {
|
|
1218
1776
|
grouped[err.path].errors.push(err.error);
|
|
@@ -1226,13 +1784,53 @@ export function groupErrorsByPath(errors) {
|
|
|
1226
1784
|
return grouped;
|
|
1227
1785
|
}
|
|
1228
1786
|
export const MetadataStore = new MetadataStoreClass();
|
|
1787
|
+
const UNSUPPORTED_SCHEMA_KEYWORDS = [
|
|
1788
|
+
'enum',
|
|
1789
|
+
'oneOf',
|
|
1790
|
+
'not',
|
|
1791
|
+
'if',
|
|
1792
|
+
'then',
|
|
1793
|
+
'else',
|
|
1794
|
+
'patternProperties',
|
|
1795
|
+
'propertyNames',
|
|
1796
|
+
'dependencies',
|
|
1797
|
+
'dependentRequired',
|
|
1798
|
+
'dependentSchemas',
|
|
1799
|
+
'contains',
|
|
1800
|
+
'minContains',
|
|
1801
|
+
'maxContains',
|
|
1802
|
+
'prefixItems',
|
|
1803
|
+
'unevaluatedProperties',
|
|
1804
|
+
'unevaluatedItems'
|
|
1805
|
+
];
|
|
1229
1806
|
export function compileSchema(schema) {
|
|
1230
1807
|
const rootDefs = schema.$defs || schema.definitions || {};
|
|
1231
1808
|
const compiledDefs = new Map();
|
|
1232
1809
|
function build(subSchema) {
|
|
1233
|
-
if (
|
|
1810
|
+
if (subSchema === true || subSchema === undefined) {
|
|
1234
1811
|
return (v) => v;
|
|
1235
1812
|
}
|
|
1813
|
+
if (subSchema === false) {
|
|
1814
|
+
return (v, path, ctx) => {
|
|
1815
|
+
report(ctx, path, 'Schema<false>', v);
|
|
1816
|
+
return v;
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
if (!subSchema || typeof subSchema !== 'object') {
|
|
1820
|
+
throw new Error('Invalid JSON Schema: subschemas must be objects or booleans');
|
|
1821
|
+
}
|
|
1822
|
+
for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
|
|
1823
|
+
if (keyword in subSchema) {
|
|
1824
|
+
throw new Error(`Unsupported JSON Schema keyword: ${keyword}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (Array.isArray(subSchema.type)) {
|
|
1828
|
+
throw new Error('Unsupported JSON Schema keyword: type arrays');
|
|
1829
|
+
}
|
|
1830
|
+
if (typeof subSchema.type === 'string' &&
|
|
1831
|
+
!['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'].includes(subSchema.type)) {
|
|
1832
|
+
throw new Error(`Unsupported JSON Schema type: ${subSchema.type}`);
|
|
1833
|
+
}
|
|
1236
1834
|
if (subSchema.$ref) {
|
|
1237
1835
|
const refPath = subSchema.$ref;
|
|
1238
1836
|
if (compiledDefs.has(refPath)) {
|
|
@@ -1294,31 +1892,90 @@ export function compileSchema(schema) {
|
|
|
1294
1892
|
return v;
|
|
1295
1893
|
};
|
|
1296
1894
|
}
|
|
1895
|
+
if ('x-typescript-type' in subSchema) {
|
|
1896
|
+
throw new Error(`Unsupported x-typescript-type: ${subSchema['x-typescript-type']}`);
|
|
1897
|
+
}
|
|
1297
1898
|
if (subSchema.allOf) {
|
|
1298
1899
|
const checks = subSchema.allOf.map((s) => build(s));
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1900
|
+
const mergeKeys = subSchema.allOf.map((s) => {
|
|
1901
|
+
if (s?.type !== 'object' ||
|
|
1902
|
+
('additionalProperties' in s && s.additionalProperties !== false)) {
|
|
1903
|
+
return undefined;
|
|
1303
1904
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1905
|
+
return new Set(Object.keys(s.properties || {}));
|
|
1906
|
+
});
|
|
1907
|
+
const allObjectSchemas = subSchema.allOf.length > 0 &&
|
|
1908
|
+
subSchema.allOf.every((s) => s?.type === 'object');
|
|
1909
|
+
const allowsAdditional = !allObjectSchemas ||
|
|
1910
|
+
subSchema.allOf.some((s) => 'additionalProperties' in s && s.additionalProperties !== false);
|
|
1911
|
+
const combinedKeys = allowsAdditional
|
|
1912
|
+
? undefined
|
|
1913
|
+
: new Set(subSchema.allOf.flatMap((s) => Object.keys(s.properties || {})));
|
|
1914
|
+
return (v, path, ctx) => {
|
|
1915
|
+
const errors = [];
|
|
1916
|
+
let data = undefined;
|
|
1917
|
+
const subCtx = {
|
|
1918
|
+
success: true,
|
|
1919
|
+
errors: [],
|
|
1920
|
+
mode: 'relaxed',
|
|
1921
|
+
from: ctx.from,
|
|
1922
|
+
mutate: false,
|
|
1923
|
+
root: ctx.root
|
|
1924
|
+
};
|
|
1925
|
+
for (let i = 0; i < checks.length; i++) {
|
|
1926
|
+
const check = checks[i];
|
|
1927
|
+
subCtx.success = true;
|
|
1928
|
+
subCtx.errors.length = 0;
|
|
1929
|
+
subCtx.from = ctx.from;
|
|
1930
|
+
subCtx.root = ctx.root;
|
|
1931
|
+
const val = check(v, path, subCtx);
|
|
1932
|
+
errors.push(...subCtx.errors);
|
|
1933
|
+
if (data === undefined) {
|
|
1934
|
+
data = isPlainObject(val) ? {} : val;
|
|
1935
|
+
}
|
|
1307
1936
|
if (isPlainObject(val) && isPlainObject(data)) {
|
|
1308
|
-
Object.
|
|
1937
|
+
const keys = mergeKeys[i] || new Set(Object.keys(val));
|
|
1938
|
+
for (const key of keys) {
|
|
1939
|
+
if (Object.hasOwn(val, key)) {
|
|
1940
|
+
setOwnProperty(data, key, val[key]);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1309
1943
|
}
|
|
1310
|
-
else {
|
|
1944
|
+
else if (data !== val) {
|
|
1311
1945
|
data = val;
|
|
1312
1946
|
}
|
|
1313
1947
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1948
|
+
if (errors.length > 0) {
|
|
1949
|
+
ctx.success = false;
|
|
1950
|
+
ctx.errors.push(...errors);
|
|
1951
|
+
return v;
|
|
1952
|
+
}
|
|
1953
|
+
if (combinedKeys && isPlainObject(v) && isPlainObject(data)) {
|
|
1954
|
+
for (const key of Object.keys(v)) {
|
|
1955
|
+
if (combinedKeys.has(key)) {
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (ctx.mode === 'strict') {
|
|
1959
|
+
report(ctx, path, `PropertyNotAllowed<${key}>`, v[key]);
|
|
1960
|
+
}
|
|
1961
|
+
else if (ctx.mode === 'strip') {
|
|
1962
|
+
delete data[key];
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (!ctx.success) {
|
|
1967
|
+
return v;
|
|
1968
|
+
}
|
|
1969
|
+
if (shouldMutate(ctx) && commitContainer(v, data)) {
|
|
1970
|
+
return v;
|
|
1971
|
+
}
|
|
1972
|
+
return data === undefined ? v : data;
|
|
1316
1973
|
};
|
|
1317
1974
|
}
|
|
1318
1975
|
if (subSchema.type === 'string') {
|
|
1319
1976
|
const minLength = subSchema.minLength;
|
|
1320
1977
|
const maxLength = subSchema.maxLength;
|
|
1321
|
-
const pattern = subSchema.pattern ?
|
|
1978
|
+
const pattern = subSchema.pattern ? createSafeRegex(subSchema.pattern) : undefined;
|
|
1322
1979
|
const patternStr = subSchema.pattern;
|
|
1323
1980
|
const format = subSchema.format;
|
|
1324
1981
|
return (v, path, ctx) => {
|
|
@@ -1419,7 +2076,7 @@ export function compileSchema(schema) {
|
|
|
1419
2076
|
const check = build(s);
|
|
1420
2077
|
return [key, isOptional, check];
|
|
1421
2078
|
});
|
|
1422
|
-
const knownKeys = Object.keys(subSchema.properties || {});
|
|
2079
|
+
const knownKeys = new Set(Object.keys(subSchema.properties || {}));
|
|
1423
2080
|
const additional = 'additionalProperties' in subSchema
|
|
1424
2081
|
? subSchema.additionalProperties
|
|
1425
2082
|
: false;
|