@webergency-utils/typechecker 0.2.1 → 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 +99 -21
- 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/index.d.ts +14 -16
- package/dist/index.js +18 -2
- package/dist/runtime/casing.js +18 -1
- package/dist/runtime/validators.d.ts +48 -15
- package/dist/runtime/validators.js +781 -111
- package/dist/transformer.js +102 -74
- 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);
|
|
@@ -44,28 +178,38 @@ function wantsQuery(ctx) {
|
|
|
44
178
|
function wantsJsonRevive(ctx) {
|
|
45
179
|
return ctx.from === 'json' || ctx.from === 'query';
|
|
46
180
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
181
|
+
function isIndexSegment(seg) {
|
|
182
|
+
return seg.startsWith('[') && seg.endsWith(']');
|
|
183
|
+
}
|
|
184
|
+
function pathContext(path, ctx) {
|
|
185
|
+
const pathParts = tokenizePath(path);
|
|
186
|
+
const parentPath = joinPathSegments(pathParts.slice(0, -1));
|
|
187
|
+
const parent = getValueAtPath(ctx.root, parentPath);
|
|
188
|
+
const last = pathParts[pathParts.length - 1];
|
|
189
|
+
let index;
|
|
190
|
+
if (last && isIndexSegment(last)) {
|
|
191
|
+
const parsed = parseInt(last.slice(1, -1), 10);
|
|
192
|
+
if (!Number.isNaN(parsed)) {
|
|
193
|
+
index = parsed;
|
|
194
|
+
}
|
|
50
195
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return path.slice(bracket + 1, end);
|
|
196
|
+
let key = '';
|
|
197
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
198
|
+
if (!isIndexSegment(pathParts[i])) {
|
|
199
|
+
key = pathParts[i];
|
|
200
|
+
break;
|
|
57
201
|
}
|
|
58
202
|
}
|
|
59
|
-
if (
|
|
60
|
-
return path.
|
|
203
|
+
if (index === undefined) {
|
|
204
|
+
return { key, path, parent, root: ctx.root };
|
|
61
205
|
}
|
|
62
|
-
return path;
|
|
206
|
+
return { key, path, parent, root: ctx.root, index };
|
|
63
207
|
}
|
|
64
|
-
function fromCustom(ctx, path, value,
|
|
208
|
+
function fromCustom(ctx, path, value, kind) {
|
|
65
209
|
if (typeof ctx.from !== 'function') {
|
|
66
210
|
return value;
|
|
67
211
|
}
|
|
68
|
-
return ctx.from(
|
|
212
|
+
return ctx.from(value, { ...pathContext(path, ctx), kind });
|
|
69
213
|
}
|
|
70
214
|
/** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
|
|
71
215
|
export function coerceQueryNumber(v) {
|
|
@@ -73,8 +217,12 @@ export function coerceQueryNumber(v) {
|
|
|
73
217
|
return v;
|
|
74
218
|
}
|
|
75
219
|
if (typeof v === 'string' && v.trim() !== '') {
|
|
76
|
-
const
|
|
77
|
-
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)) {
|
|
78
226
|
return parsed;
|
|
79
227
|
}
|
|
80
228
|
}
|
|
@@ -279,17 +427,32 @@ function isUriTemplate(value) {
|
|
|
279
427
|
return URI_TEMPLATE_RE.test(value);
|
|
280
428
|
}
|
|
281
429
|
function parseFormatDate(value) {
|
|
282
|
-
if (typeof value !== 'string'
|
|
430
|
+
if (typeof value !== 'string') {
|
|
283
431
|
return undefined;
|
|
284
432
|
}
|
|
285
|
-
const
|
|
286
|
-
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) {
|
|
287
446
|
return undefined;
|
|
288
447
|
}
|
|
289
448
|
return parsed;
|
|
290
449
|
}
|
|
291
450
|
function parseFormatDateTime(value) {
|
|
292
|
-
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))) {
|
|
293
456
|
return undefined;
|
|
294
457
|
}
|
|
295
458
|
const parsed = new Date(value);
|
|
@@ -298,30 +461,236 @@ function parseFormatDateTime(value) {
|
|
|
298
461
|
}
|
|
299
462
|
return parsed;
|
|
300
463
|
}
|
|
301
|
-
function
|
|
302
|
-
if (
|
|
303
|
-
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;
|
|
474
|
+
}
|
|
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);
|
|
304
574
|
}
|
|
305
|
-
if (
|
|
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)) {
|
|
306
632
|
return undefined;
|
|
307
633
|
}
|
|
308
|
-
seen.add(value);
|
|
309
634
|
if (Array.isArray(value)) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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;
|
|
315
650
|
}
|
|
316
|
-
const keys = Object.keys(value)
|
|
317
|
-
const
|
|
318
|
-
|
|
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
|
+
}
|
|
665
|
+
}
|
|
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;
|
|
319
683
|
}
|
|
320
684
|
export const validators = {
|
|
321
685
|
coerceQueryNumber,
|
|
322
686
|
coerceQueryBoolean,
|
|
323
687
|
coerceQueryDate,
|
|
324
688
|
coerceJsonDate,
|
|
689
|
+
safeRegExp: createSafeRegex,
|
|
690
|
+
assign: (target, source) => {
|
|
691
|
+
assignOwnProperties(target, source);
|
|
692
|
+
return target;
|
|
693
|
+
},
|
|
325
694
|
string: (v, path, ctx) => {
|
|
326
695
|
if (typeof v === 'string') {
|
|
327
696
|
return v;
|
|
@@ -519,7 +888,7 @@ export const validators = {
|
|
|
519
888
|
},
|
|
520
889
|
array: (v, path, ctx, childValidator) => {
|
|
521
890
|
if (!Array.isArray(v)) {
|
|
522
|
-
if (ctx
|
|
891
|
+
if (wantsQuery(ctx) && v !== undefined && v !== null) {
|
|
523
892
|
v = [v];
|
|
524
893
|
}
|
|
525
894
|
else if (typeof ctx.from === 'function') {
|
|
@@ -537,11 +906,15 @@ export const validators = {
|
|
|
537
906
|
return v;
|
|
538
907
|
}
|
|
539
908
|
}
|
|
540
|
-
|
|
541
|
-
|
|
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 = [];
|
|
542
916
|
for (let i = 0; i < v.length; i++) {
|
|
543
|
-
|
|
544
|
-
data[i] = val;
|
|
917
|
+
data[i] = childValidator(v[i], path + '[' + i + ']', ctx);
|
|
545
918
|
}
|
|
546
919
|
return data;
|
|
547
920
|
},
|
|
@@ -552,7 +925,7 @@ export const validators = {
|
|
|
552
925
|
const wasSuccess = ctx.success;
|
|
553
926
|
const result = validator(val, path + '.' + key, ctx);
|
|
554
927
|
if (ctx.success) {
|
|
555
|
-
data
|
|
928
|
+
setOwnProperty(data, key, result);
|
|
556
929
|
}
|
|
557
930
|
else if (isOptional && val === undefined) {
|
|
558
931
|
ctx.errors.length = oldErrors;
|
|
@@ -577,7 +950,7 @@ export const validators = {
|
|
|
577
950
|
return data;
|
|
578
951
|
}
|
|
579
952
|
for (const k of Object.keys(data)) {
|
|
580
|
-
if (!allowedKeys
|
|
953
|
+
if (!keySetHas(allowedKeys, k)) {
|
|
581
954
|
delete data[k];
|
|
582
955
|
}
|
|
583
956
|
}
|
|
@@ -588,10 +961,10 @@ export const validators = {
|
|
|
588
961
|
return;
|
|
589
962
|
}
|
|
590
963
|
for (const key of Object.keys(v)) {
|
|
591
|
-
if (knownKeys
|
|
964
|
+
if (keySetHas(knownKeys, key)) {
|
|
592
965
|
continue;
|
|
593
966
|
}
|
|
594
|
-
data
|
|
967
|
+
setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
|
|
595
968
|
}
|
|
596
969
|
},
|
|
597
970
|
object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
|
|
@@ -613,7 +986,7 @@ export const validators = {
|
|
|
613
986
|
}
|
|
614
987
|
if (ctx.mode === 'strict' && allowedKeys) {
|
|
615
988
|
for (const k of Object.keys(v)) {
|
|
616
|
-
if (!allowedKeys
|
|
989
|
+
if (!keySetHas(allowedKeys, k)) {
|
|
617
990
|
report(ctx, path, `PropertyNotAllowed<${k}>`, v[k]);
|
|
618
991
|
}
|
|
619
992
|
}
|
|
@@ -664,7 +1037,13 @@ export const validators = {
|
|
|
664
1037
|
},
|
|
665
1038
|
multipleOf: (v, path, ctx, n, message) => {
|
|
666
1039
|
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
667
|
-
|
|
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 {
|
|
668
1047
|
report(ctx, path, `MultipleOf<${n}>`, v, message);
|
|
669
1048
|
}
|
|
670
1049
|
}
|
|
@@ -674,6 +1053,10 @@ export const validators = {
|
|
|
674
1053
|
return v;
|
|
675
1054
|
},
|
|
676
1055
|
pattern: (v, path, ctx, regex, expected, message) => {
|
|
1056
|
+
if (!isRegexSafe(regex)) {
|
|
1057
|
+
report(ctx, path, 'UnsafePattern', v, message);
|
|
1058
|
+
return v;
|
|
1059
|
+
}
|
|
677
1060
|
if (!testRegex(regex, v)) {
|
|
678
1061
|
report(ctx, path, expected, v, message);
|
|
679
1062
|
}
|
|
@@ -730,7 +1113,7 @@ export const validators = {
|
|
|
730
1113
|
case 'password': break; // Anything is a password
|
|
731
1114
|
case 'regex':
|
|
732
1115
|
try {
|
|
733
|
-
|
|
1116
|
+
createSafeRegex(v);
|
|
734
1117
|
}
|
|
735
1118
|
catch {
|
|
736
1119
|
isValid = false;
|
|
@@ -792,39 +1175,181 @@ export const validators = {
|
|
|
792
1175
|
return v;
|
|
793
1176
|
},
|
|
794
1177
|
uniqueItems: (v, path, ctx, message) => {
|
|
795
|
-
|
|
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 = [];
|
|
796
1194
|
for (let i = 0; i < v.length; i++) {
|
|
797
1195
|
const item = v[i];
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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;
|
|
804
1221
|
}
|
|
805
|
-
|
|
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;
|
|
1255
|
+
}
|
|
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);
|
|
806
1319
|
}
|
|
807
1320
|
return v;
|
|
808
1321
|
},
|
|
809
1322
|
custom: (v, path, ctx, fn, message) => {
|
|
810
|
-
|
|
811
|
-
const parentPath = joinPathSegments(pathParts.slice(0, -1));
|
|
812
|
-
const parent = getValueAtPath(ctx.root, parentPath);
|
|
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 })) {
|
|
1323
|
+
if (!fn(v, pathContext(path, ctx))) {
|
|
816
1324
|
report(ctx, path, fn.name ? `Custom<${fn.name}>` : 'Custom', v, message);
|
|
817
1325
|
}
|
|
818
1326
|
return v;
|
|
819
1327
|
},
|
|
820
1328
|
union: (v, path, ctx, checks, expected = 'Type<Union>') => {
|
|
821
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
|
+
};
|
|
822
1345
|
// Pass 1: No conversion
|
|
823
1346
|
for (const check of checks) {
|
|
824
|
-
|
|
1347
|
+
subCtx.success = true;
|
|
1348
|
+
subCtx.errors.length = 0;
|
|
1349
|
+
subCtx.from = undefined;
|
|
825
1350
|
const val = check(v, path, subCtx);
|
|
826
1351
|
if (subCtx.success) {
|
|
827
|
-
return val;
|
|
1352
|
+
return accept(val);
|
|
828
1353
|
}
|
|
829
1354
|
unionErrors.push(...subCtx.errors);
|
|
830
1355
|
}
|
|
@@ -832,10 +1357,12 @@ export const validators = {
|
|
|
832
1357
|
if (ctx.from) {
|
|
833
1358
|
unionErrors.length = 0;
|
|
834
1359
|
for (const check of checks) {
|
|
835
|
-
|
|
1360
|
+
subCtx.success = true;
|
|
1361
|
+
subCtx.errors.length = 0;
|
|
1362
|
+
subCtx.from = ctx.from;
|
|
836
1363
|
const val = check(v, path, subCtx);
|
|
837
1364
|
if (subCtx.success) {
|
|
838
|
-
return val;
|
|
1365
|
+
return accept(val);
|
|
839
1366
|
}
|
|
840
1367
|
unionErrors.push(...subCtx.errors);
|
|
841
1368
|
}
|
|
@@ -866,8 +1393,13 @@ export const validators = {
|
|
|
866
1393
|
return v;
|
|
867
1394
|
}
|
|
868
1395
|
}
|
|
869
|
-
|
|
870
|
-
|
|
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 = [];
|
|
871
1403
|
for (let i = 0; i < checks.length; i++) {
|
|
872
1404
|
data[i] = checks[i](v[i], path + '[' + i + ']', ctx);
|
|
873
1405
|
}
|
|
@@ -940,7 +1472,7 @@ export const validators = {
|
|
|
940
1472
|
const mutate = shouldMutate(ctx);
|
|
941
1473
|
const data = mutate ? v : {};
|
|
942
1474
|
for (const key of Object.keys(v)) {
|
|
943
|
-
data
|
|
1475
|
+
setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
|
|
944
1476
|
}
|
|
945
1477
|
return data;
|
|
946
1478
|
},
|
|
@@ -967,13 +1499,17 @@ export const validators = {
|
|
|
967
1499
|
return v;
|
|
968
1500
|
}
|
|
969
1501
|
}
|
|
970
|
-
const mutate = shouldMutate(ctx);
|
|
971
1502
|
const source = [...v];
|
|
972
|
-
|
|
1503
|
+
let index = 0;
|
|
1504
|
+
if (shouldMutate(ctx)) {
|
|
973
1505
|
v.clear();
|
|
1506
|
+
for (const item of source) {
|
|
1507
|
+
v.add(childValidator(item, `${path}[${index}]`, ctx));
|
|
1508
|
+
index++;
|
|
1509
|
+
}
|
|
1510
|
+
return v;
|
|
974
1511
|
}
|
|
975
|
-
const data =
|
|
976
|
-
let index = 0;
|
|
1512
|
+
const data = new Set();
|
|
977
1513
|
for (const item of source) {
|
|
978
1514
|
data.add(childValidator(item, `${path}[${index}]`, ctx));
|
|
979
1515
|
index++;
|
|
@@ -1000,12 +1536,17 @@ export const validators = {
|
|
|
1000
1536
|
return v;
|
|
1001
1537
|
}
|
|
1002
1538
|
}
|
|
1003
|
-
const mutate = shouldMutate(ctx);
|
|
1004
1539
|
const source = [...v.entries()];
|
|
1005
|
-
if (
|
|
1540
|
+
if (shouldMutate(ctx)) {
|
|
1006
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;
|
|
1007
1548
|
}
|
|
1008
|
-
const data =
|
|
1549
|
+
const data = new Map();
|
|
1009
1550
|
for (const [key, val] of source) {
|
|
1010
1551
|
const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
|
|
1011
1552
|
const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
|
|
@@ -1132,8 +1673,11 @@ export class MetadataStoreClass {
|
|
|
1132
1673
|
return schema;
|
|
1133
1674
|
}
|
|
1134
1675
|
getOrCompileSchema(schema) {
|
|
1676
|
+
if (typeof schema === 'boolean') {
|
|
1677
|
+
return compileSchema(schema);
|
|
1678
|
+
}
|
|
1135
1679
|
if (typeof schema !== 'object' || schema === null) {
|
|
1136
|
-
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');
|
|
1137
1681
|
}
|
|
1138
1682
|
let compiled = this.compiledSchemas.get(schema);
|
|
1139
1683
|
if (!compiled) {
|
|
@@ -1145,20 +1689,24 @@ export class MetadataStoreClass {
|
|
|
1145
1689
|
is(validator, value, options) {
|
|
1146
1690
|
const opt = options;
|
|
1147
1691
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1692
|
+
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1693
|
+
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1694
|
+
const res = validator(value, '', ctx);
|
|
1695
|
+
if (!ctx.success) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
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);
|
|
1154
1703
|
}
|
|
1155
1704
|
assert(validator, value, options) {
|
|
1156
1705
|
const opt = options;
|
|
1157
1706
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1158
1707
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1159
|
-
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
1160
1708
|
const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
|
|
1161
|
-
const ctx = { success: true, errors: [], mode, from,
|
|
1709
|
+
const ctx = { success: true, errors: [], mode, from, mutate, root: value };
|
|
1162
1710
|
const res = validator(value, '', ctx);
|
|
1163
1711
|
if (!ctx.success) {
|
|
1164
1712
|
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
@@ -1166,40 +1714,63 @@ export class MetadataStoreClass {
|
|
|
1166
1714
|
}
|
|
1167
1715
|
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
1168
1716
|
}
|
|
1717
|
+
if (mutate) {
|
|
1718
|
+
if (res === value || commitContainer(value, res)) {
|
|
1719
|
+
return value;
|
|
1720
|
+
}
|
|
1721
|
+
return res;
|
|
1722
|
+
}
|
|
1169
1723
|
return res;
|
|
1170
1724
|
}
|
|
1171
1725
|
assertGuard(validator, value, options) {
|
|
1172
1726
|
const opt = options;
|
|
1173
1727
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1728
|
+
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1729
|
+
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1730
|
+
const res = validator(value, '', ctx);
|
|
1731
|
+
if (ctx.success && (res === value || commitContainer(value, res))) {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
// Root was replaced (e.g. primitive coerce) — binding unchanged; report a normal type failure.
|
|
1735
|
+
if (ctx.success && res !== value) {
|
|
1736
|
+
ctx.success = true;
|
|
1737
|
+
ctx.errors.length = 0;
|
|
1738
|
+
ctx.from = undefined;
|
|
1739
|
+
validator(value, '', ctx);
|
|
1740
|
+
if (ctx.success) {
|
|
1741
|
+
report(ctx, '', 'RootNotRewritable', value);
|
|
1182
1742
|
}
|
|
1183
|
-
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
1184
1743
|
}
|
|
1744
|
+
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
1745
|
+
throw opt.errorFactory(ctx.errors);
|
|
1746
|
+
}
|
|
1747
|
+
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
1185
1748
|
}
|
|
1186
1749
|
validate(validator, value, options) {
|
|
1187
1750
|
const opt = options;
|
|
1188
1751
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1189
1752
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1190
|
-
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
1191
1753
|
const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
|
|
1192
|
-
const ctx = { success: true, errors: [], mode, from,
|
|
1754
|
+
const ctx = { success: true, errors: [], mode, from, mutate, root: value };
|
|
1193
1755
|
const res = validator(value, '', ctx);
|
|
1194
|
-
|
|
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 };
|
|
1195
1766
|
}
|
|
1196
1767
|
}
|
|
1197
1768
|
export function groupErrorsByPath(errors) {
|
|
1198
|
-
const grouped =
|
|
1769
|
+
const grouped = Object.create(null);
|
|
1199
1770
|
const visit = (list) => {
|
|
1200
1771
|
for (const err of list) {
|
|
1201
|
-
if (!grouped
|
|
1202
|
-
grouped
|
|
1772
|
+
if (!Object.hasOwn(grouped, err.path)) {
|
|
1773
|
+
setOwnProperty(grouped, err.path, { value: err.value, errors: [] });
|
|
1203
1774
|
}
|
|
1204
1775
|
if (!grouped[err.path].errors.includes(err.error)) {
|
|
1205
1776
|
grouped[err.path].errors.push(err.error);
|
|
@@ -1213,13 +1784,53 @@ export function groupErrorsByPath(errors) {
|
|
|
1213
1784
|
return grouped;
|
|
1214
1785
|
}
|
|
1215
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
|
+
];
|
|
1216
1806
|
export function compileSchema(schema) {
|
|
1217
1807
|
const rootDefs = schema.$defs || schema.definitions || {};
|
|
1218
1808
|
const compiledDefs = new Map();
|
|
1219
1809
|
function build(subSchema) {
|
|
1220
|
-
if (
|
|
1810
|
+
if (subSchema === true || subSchema === undefined) {
|
|
1221
1811
|
return (v) => v;
|
|
1222
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
|
+
}
|
|
1223
1834
|
if (subSchema.$ref) {
|
|
1224
1835
|
const refPath = subSchema.$ref;
|
|
1225
1836
|
if (compiledDefs.has(refPath)) {
|
|
@@ -1281,31 +1892,90 @@ export function compileSchema(schema) {
|
|
|
1281
1892
|
return v;
|
|
1282
1893
|
};
|
|
1283
1894
|
}
|
|
1895
|
+
if ('x-typescript-type' in subSchema) {
|
|
1896
|
+
throw new Error(`Unsupported x-typescript-type: ${subSchema['x-typescript-type']}`);
|
|
1897
|
+
}
|
|
1284
1898
|
if (subSchema.allOf) {
|
|
1285
1899
|
const checks = subSchema.allOf.map((s) => build(s));
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1900
|
+
const mergeKeys = subSchema.allOf.map((s) => {
|
|
1901
|
+
if (s?.type !== 'object' ||
|
|
1902
|
+
('additionalProperties' in s && s.additionalProperties !== false)) {
|
|
1903
|
+
return undefined;
|
|
1290
1904
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
}
|
|
1294
1936
|
if (isPlainObject(val) && isPlainObject(data)) {
|
|
1295
|
-
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
|
+
}
|
|
1296
1943
|
}
|
|
1297
|
-
else {
|
|
1944
|
+
else if (data !== val) {
|
|
1298
1945
|
data = val;
|
|
1299
1946
|
}
|
|
1300
1947
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
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;
|
|
1303
1973
|
};
|
|
1304
1974
|
}
|
|
1305
1975
|
if (subSchema.type === 'string') {
|
|
1306
1976
|
const minLength = subSchema.minLength;
|
|
1307
1977
|
const maxLength = subSchema.maxLength;
|
|
1308
|
-
const pattern = subSchema.pattern ?
|
|
1978
|
+
const pattern = subSchema.pattern ? createSafeRegex(subSchema.pattern) : undefined;
|
|
1309
1979
|
const patternStr = subSchema.pattern;
|
|
1310
1980
|
const format = subSchema.format;
|
|
1311
1981
|
return (v, path, ctx) => {
|
|
@@ -1406,7 +2076,7 @@ export function compileSchema(schema) {
|
|
|
1406
2076
|
const check = build(s);
|
|
1407
2077
|
return [key, isOptional, check];
|
|
1408
2078
|
});
|
|
1409
|
-
const knownKeys = Object.keys(subSchema.properties || {});
|
|
2079
|
+
const knownKeys = new Set(Object.keys(subSchema.properties || {}));
|
|
1410
2080
|
const additional = 'additionalProperties' in subSchema
|
|
1411
2081
|
? subSchema.additionalProperties
|
|
1412
2082
|
: false;
|