@oh-my-pi/omptype 17.2.7 → 17.2.8
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/CHANGELOG.md +23 -0
- package/dist/js/compile.js +414 -219
- package/dist/js/errors.js +236 -41
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +1 -0
- package/dist/js/interp.js +514 -132
- package/dist/js/ir.js +972 -202
- package/dist/js/json-schema.js +73 -34
- package/dist/js/keywords.js +108 -1
- package/dist/js/type.js +2156 -172
- package/dist/js/typebox.js +41 -32
- package/dist/js/zod.js +2 -2
- package/dist/types/compile.d.ts +1 -1
- package/dist/types/errors.d.ts +19 -18
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/infer.d.ts +45 -6
- package/dist/types/interp.d.ts +9 -2
- package/dist/types/ir.d.ts +57 -6
- package/dist/types/json-schema.d.ts +10 -1
- package/dist/types/type.d.ts +324 -48
- package/dist/types/typebox.d.ts +78 -50
- package/package.json +5 -1
- package/src/compile.ts +598 -243
- package/src/errors.ts +247 -49
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +1 -0
- package/src/infer.ts +107 -32
- package/src/interp.ts +457 -118
- package/src/ir.ts +981 -212
- package/src/json-schema.ts +82 -34
- package/src/keywords.ts +111 -1
- package/src/type.ts +2760 -271
- package/src/typebox.ts +141 -98
- package/src/zod.ts +1 -1
package/src/interp.ts
CHANGED
|
@@ -9,15 +9,36 @@
|
|
|
9
9
|
* - failure returns an `OmpErrors` with a single fast-fail entry
|
|
10
10
|
*/
|
|
11
11
|
import { MISSING, OmpErrors } from "./errors";
|
|
12
|
-
import { expectedOf, hasMorph, type IR } from "./ir";
|
|
12
|
+
import { expectedOf, hasAlias, hasMorph, type IR } from "./ir";
|
|
13
13
|
|
|
14
14
|
const own = Object.prototype.hasOwnProperty;
|
|
15
|
+
/** Return an independent runtime value for a prevalidated static default. */
|
|
16
|
+
export function materializeDefault(payload: unknown): unknown {
|
|
17
|
+
if (payload === null || typeof payload !== "object") return payload;
|
|
18
|
+
if (payload instanceof Date) return new Date(payload);
|
|
19
|
+
return structuredClone(payload);
|
|
20
|
+
}
|
|
21
|
+
let activeVisits: WeakMap<object, Set<IR>> | undefined;
|
|
22
|
+
let activeChecks: WeakMap<object, Set<IR>> | undefined;
|
|
15
23
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Validate `value` against `ir`; returns output value or `OmpErrors`.
|
|
26
|
+
* `path` seeds the traversal location so nested step callbacks observe
|
|
27
|
+
* absolute ctx.path values when a compiled parent delegates a subtree;
|
|
28
|
+
* resulting error paths are then already absolute.
|
|
29
|
+
*/
|
|
30
|
+
export function walk(ir: IR, value: unknown, path: PropertyKey[] = []): unknown {
|
|
31
|
+
const previousVisits = activeVisits;
|
|
32
|
+
const previousChecks = activeChecks;
|
|
33
|
+
// Created lazily by visit/checks only when a recursive-alias node is reached.
|
|
34
|
+
activeVisits = undefined;
|
|
35
|
+
activeChecks = undefined;
|
|
36
|
+
try {
|
|
37
|
+
return visit(ir, value, path);
|
|
38
|
+
} finally {
|
|
39
|
+
activeVisits = previousVisits;
|
|
40
|
+
activeChecks = previousChecks;
|
|
41
|
+
}
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
|
|
@@ -27,6 +48,26 @@ function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
|
|
|
27
48
|
|
|
28
49
|
/** Pure predicate used for union-member scanning (no morphs, no errors). */
|
|
29
50
|
function checks(ir: IR, v: unknown): boolean {
|
|
51
|
+
// Cycle guards are only needed when the subtree can revisit nodes through
|
|
52
|
+
// recursive aliases; plain schemas skip the WeakMap bookkeeping entirely.
|
|
53
|
+
if (typeof v !== "object" || v === null || !hasAlias(ir)) return checkNode(ir, v);
|
|
54
|
+
activeChecks ??= new WeakMap();
|
|
55
|
+
const visits = activeChecks;
|
|
56
|
+
let visited = visits.get(v);
|
|
57
|
+
if (visited?.has(ir)) return true;
|
|
58
|
+
if (visited === undefined) {
|
|
59
|
+
visited = new Set();
|
|
60
|
+
visits.set(v, visited);
|
|
61
|
+
}
|
|
62
|
+
visited.add(ir);
|
|
63
|
+
try {
|
|
64
|
+
return checkNode(ir, v);
|
|
65
|
+
} finally {
|
|
66
|
+
visited.delete(ir);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkNode(ir: IR, v: unknown): boolean {
|
|
30
71
|
switch (ir.k) {
|
|
31
72
|
case "unknown":
|
|
32
73
|
return true;
|
|
@@ -69,7 +110,7 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
69
110
|
if (!Array.isArray(v)) return false;
|
|
70
111
|
if (ir.min !== undefined && v.length < ir.min) return false;
|
|
71
112
|
if (ir.max !== undefined && v.length > ir.max) return false;
|
|
72
|
-
for (const
|
|
113
|
+
for (const element of v) if (!checks(ir.el, element)) return false;
|
|
73
114
|
return true;
|
|
74
115
|
}
|
|
75
116
|
case "tuple": {
|
|
@@ -98,8 +139,8 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
98
139
|
return true;
|
|
99
140
|
}
|
|
100
141
|
case "object": {
|
|
101
|
-
if (typeof v !== "object" || v === null
|
|
102
|
-
const rec = v as Record<
|
|
142
|
+
if (typeof v !== "object" || v === null) return false;
|
|
143
|
+
const rec = v as Record<PropertyKey, unknown>;
|
|
103
144
|
for (const p of ir.props) {
|
|
104
145
|
const present = p.key in rec;
|
|
105
146
|
if (!present) {
|
|
@@ -108,21 +149,31 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
108
149
|
}
|
|
109
150
|
if (!checks(p.val, rec[p.key])) return false;
|
|
110
151
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (p.key === key) {
|
|
121
|
-
declared = true;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
152
|
+
for (const key in rec) {
|
|
153
|
+
if (!own.call(rec, key)) continue;
|
|
154
|
+
if (ir.index !== undefined && !checks(ir.index, rec[key])) return false;
|
|
155
|
+
let patternMatched = false;
|
|
156
|
+
if (ir.patternIndexes !== undefined) {
|
|
157
|
+
for (const pattern of ir.patternIndexes) {
|
|
158
|
+
if (!checks(pattern.key, key)) continue;
|
|
159
|
+
patternMatched = true;
|
|
160
|
+
if (!checks(pattern.val, rec[key])) return false;
|
|
124
161
|
}
|
|
125
|
-
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
ir.extras === "reject" &&
|
|
165
|
+
ir.index === undefined &&
|
|
166
|
+
!patternMatched &&
|
|
167
|
+
!ir.props.some(prop => prop.key === key)
|
|
168
|
+
) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const key of Object.getOwnPropertySymbols(rec)) {
|
|
173
|
+
if (!Object.prototype.propertyIsEnumerable.call(rec, key)) continue;
|
|
174
|
+
if (ir.symbolIndex !== undefined && !checks(ir.symbolIndex, rec[key])) return false;
|
|
175
|
+
if (ir.extras === "reject" && ir.symbolIndex === undefined && !ir.props.some(prop => prop.key === key)) {
|
|
176
|
+
return false;
|
|
126
177
|
}
|
|
127
178
|
}
|
|
128
179
|
return true;
|
|
@@ -132,7 +183,7 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
132
183
|
case "refine":
|
|
133
184
|
if (!checks(ir.base, v)) return false;
|
|
134
185
|
try {
|
|
135
|
-
return ir.pred(v);
|
|
186
|
+
return ir.pred(v) === true;
|
|
136
187
|
} catch {
|
|
137
188
|
return false;
|
|
138
189
|
}
|
|
@@ -146,6 +197,34 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
146
197
|
}
|
|
147
198
|
|
|
148
199
|
function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
200
|
+
if (typeof v !== "object" || v === null || !hasAlias(ir)) return visitFinish(ir, v, path);
|
|
201
|
+
activeVisits ??= new WeakMap();
|
|
202
|
+
const visits = activeVisits;
|
|
203
|
+
let visited = visits.get(v);
|
|
204
|
+
if (visited?.has(ir)) return v;
|
|
205
|
+
if (visited === undefined) {
|
|
206
|
+
visited = new Set();
|
|
207
|
+
visits.set(v, visited);
|
|
208
|
+
}
|
|
209
|
+
visited.add(ir);
|
|
210
|
+
try {
|
|
211
|
+
return visitFinish(ir, v, path);
|
|
212
|
+
} finally {
|
|
213
|
+
visited.delete(ir);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Run the node visitor, then apply node-local error configuration. */
|
|
218
|
+
function visitFinish(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
219
|
+
const out = visitNode(ir, v, path);
|
|
220
|
+
if (!(out instanceof OmpErrors) || ir.cfg === undefined) return out;
|
|
221
|
+
for (const error of out) {
|
|
222
|
+
if (error.path.length !== path.length) return out;
|
|
223
|
+
}
|
|
224
|
+
return out.configure(ir.cfg);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function visitNode(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
149
228
|
switch (ir.k) {
|
|
150
229
|
case "alias":
|
|
151
230
|
return visit(ir.resolve(), v, path);
|
|
@@ -153,7 +232,9 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
153
232
|
const base = visit(ir.base, v, path);
|
|
154
233
|
if (base instanceof OmpErrors) return base;
|
|
155
234
|
try {
|
|
156
|
-
|
|
235
|
+
const result = ir.pred(base);
|
|
236
|
+
if (result instanceof OmpErrors) return path.length === 0 ? result : prefixAll(result, path);
|
|
237
|
+
return result ? base : fail(path, ir.expected, base);
|
|
157
238
|
} catch {
|
|
158
239
|
return fail(path, ir.expected, base);
|
|
159
240
|
}
|
|
@@ -178,7 +259,7 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
178
259
|
return output;
|
|
179
260
|
}
|
|
180
261
|
case "sub": {
|
|
181
|
-
const out = ir.schema.run(v);
|
|
262
|
+
const out = ir.schema.run(v, path);
|
|
182
263
|
if (out instanceof OmpErrors) {
|
|
183
264
|
return path.length === 0 ? out : prefixAll(out, path);
|
|
184
265
|
}
|
|
@@ -192,38 +273,40 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
192
273
|
return v;
|
|
193
274
|
}
|
|
194
275
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
276
|
+
let targeted: OmpErrors | undefined;
|
|
277
|
+
let targetCount = 0;
|
|
278
|
+
for (const member of ir.members) {
|
|
279
|
+
if (member.k === "sub" || hasMorph(member)) {
|
|
280
|
+
const out = visit(member, v, path);
|
|
198
281
|
if (!(out instanceof OmpErrors)) return out;
|
|
282
|
+
if (kindMatches(unwrapBase(member), v)) {
|
|
283
|
+
targetCount++;
|
|
284
|
+
targeted ??= out;
|
|
285
|
+
}
|
|
199
286
|
}
|
|
200
287
|
}
|
|
288
|
+
if (targetCount === 1 && targeted !== undefined) return targeted;
|
|
201
289
|
return ir.members.some(canRefineUnionFailure) ? unionFail(ir, v, path) : fail(path, expectedOf(ir), v);
|
|
202
290
|
}
|
|
203
291
|
case "array": {
|
|
204
292
|
if (!Array.isArray(v)) return fail(path, "an array", v);
|
|
205
|
-
if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v);
|
|
206
|
-
if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return err instanceof OmpErrors ? err : fail([...path, i], expectedOf(ir.el), v[i]);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return v;
|
|
217
|
-
}
|
|
218
|
-
const out = new Array<unknown>(v.length);
|
|
219
|
-
for (let i = 0; i < v.length; i++) {
|
|
220
|
-
path.push(i);
|
|
221
|
-
const el = visit(ir.el, v[i], path);
|
|
293
|
+
if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v.length);
|
|
294
|
+
if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v.length);
|
|
295
|
+
const morph = hasMorph(ir.el);
|
|
296
|
+
const out = morph ? new Array<unknown>(v.length) : v;
|
|
297
|
+
let errors: OmpErrors | undefined;
|
|
298
|
+
for (let index = 0; index < v.length; index++) {
|
|
299
|
+
path.push(index);
|
|
300
|
+
const element = visit(ir.el, v[index], path);
|
|
222
301
|
path.pop();
|
|
223
|
-
if (
|
|
224
|
-
|
|
302
|
+
if (element instanceof OmpErrors) {
|
|
303
|
+
if (errors) errors.append(element);
|
|
304
|
+
else errors = element;
|
|
305
|
+
} else if (morph) {
|
|
306
|
+
out[index] = element;
|
|
307
|
+
}
|
|
225
308
|
}
|
|
226
|
-
return out;
|
|
309
|
+
return errors ?? out;
|
|
227
310
|
}
|
|
228
311
|
case "tuple": {
|
|
229
312
|
if (!Array.isArray(v)) return fail(path, "an array", v);
|
|
@@ -238,23 +321,41 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
238
321
|
const prefixCount = Math.min(ir.prefix.length, postfixStart);
|
|
239
322
|
const morph = hasMorph(ir);
|
|
240
323
|
const output = morph ? [...v] : v;
|
|
324
|
+
let errors: OmpErrors | undefined;
|
|
241
325
|
for (let index = 0; index < prefixCount; index++) {
|
|
242
326
|
path.push(index);
|
|
243
327
|
const item = visit(ir.prefix[index].val, v[index], path);
|
|
244
328
|
path.pop();
|
|
245
|
-
if (item instanceof OmpErrors)
|
|
246
|
-
|
|
329
|
+
if (item instanceof OmpErrors) {
|
|
330
|
+
if (errors) errors.append(item);
|
|
331
|
+
else errors = item;
|
|
332
|
+
} else if (morph) {
|
|
333
|
+
output[index] = item;
|
|
334
|
+
}
|
|
247
335
|
}
|
|
248
336
|
for (let index = prefixCount; index < ir.prefix.length; index++) {
|
|
249
337
|
const item = ir.prefix[index];
|
|
250
338
|
if (item.hasDefault && morph) {
|
|
251
339
|
const payload = item.def;
|
|
252
|
-
|
|
340
|
+
if (item.defFactory && typeof payload === "function") {
|
|
341
|
+
path.push(index);
|
|
342
|
+
const resolved = visit(item.val, payload(), path);
|
|
343
|
+
path.pop();
|
|
344
|
+
if (resolved instanceof OmpErrors) {
|
|
345
|
+
if (errors) errors.append(resolved);
|
|
346
|
+
else errors = resolved;
|
|
347
|
+
} else {
|
|
348
|
+
output[index] = resolved;
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
output[index] = materializeDefault(payload);
|
|
352
|
+
}
|
|
253
353
|
} else if (!item.opt) {
|
|
254
354
|
path.push(index);
|
|
255
355
|
const error = fail(path, expectedOf(item.val), MISSING);
|
|
256
356
|
path.pop();
|
|
257
|
-
|
|
357
|
+
if (errors) errors.append(error);
|
|
358
|
+
else errors = error;
|
|
258
359
|
}
|
|
259
360
|
}
|
|
260
361
|
if (ir.variadic !== undefined) {
|
|
@@ -262,8 +363,12 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
262
363
|
path.push(index);
|
|
263
364
|
const item = visit(ir.variadic, v[index], path);
|
|
264
365
|
path.pop();
|
|
265
|
-
if (item instanceof OmpErrors)
|
|
266
|
-
|
|
366
|
+
if (item instanceof OmpErrors) {
|
|
367
|
+
if (errors) errors.append(item);
|
|
368
|
+
else errors = item;
|
|
369
|
+
} else if (morph) {
|
|
370
|
+
output[index] = item;
|
|
371
|
+
}
|
|
267
372
|
}
|
|
268
373
|
}
|
|
269
374
|
for (let index = 0; index < ir.postfix.length; index++) {
|
|
@@ -271,18 +376,28 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
271
376
|
path.push(inputIndex);
|
|
272
377
|
const item = visit(ir.postfix[index], v[inputIndex], path);
|
|
273
378
|
path.pop();
|
|
274
|
-
if (item instanceof OmpErrors)
|
|
275
|
-
|
|
379
|
+
if (item instanceof OmpErrors) {
|
|
380
|
+
if (errors) errors.append(item);
|
|
381
|
+
else errors = item;
|
|
382
|
+
} else if (morph) {
|
|
383
|
+
output[inputIndex] = item;
|
|
384
|
+
}
|
|
276
385
|
}
|
|
277
|
-
return output;
|
|
386
|
+
return errors ?? output;
|
|
278
387
|
}
|
|
279
388
|
case "object": {
|
|
280
|
-
if (typeof v !== "object" || v === null
|
|
281
|
-
const rec = v as Record<
|
|
389
|
+
if (typeof v !== "object" || v === null) return fail(path, "an object", v);
|
|
390
|
+
const rec = v as Record<PropertyKey, unknown>;
|
|
282
391
|
const morph = hasMorph(ir);
|
|
283
|
-
let out: Record<
|
|
392
|
+
let out: Record<PropertyKey, unknown> | undefined;
|
|
393
|
+
let errors: OmpErrors | undefined;
|
|
284
394
|
if (morph) {
|
|
285
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
ir.extras === "delete" &&
|
|
397
|
+
ir.index === undefined &&
|
|
398
|
+
ir.symbolIndex === undefined &&
|
|
399
|
+
ir.patternIndexes === undefined
|
|
400
|
+
) {
|
|
286
401
|
out = {};
|
|
287
402
|
} else {
|
|
288
403
|
out = { ...rec };
|
|
@@ -291,51 +406,155 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
291
406
|
for (const p of ir.props) {
|
|
292
407
|
if (!(p.key in rec)) {
|
|
293
408
|
if (p.hasDefault && out) {
|
|
294
|
-
// defFactory guarantees a callable default payload
|
|
295
409
|
const payload = p.def;
|
|
296
|
-
|
|
410
|
+
if (p.defFactory && typeof payload === "function") {
|
|
411
|
+
path.push(p.key);
|
|
412
|
+
const resolved = visit(p.val, payload(), path);
|
|
413
|
+
path.pop();
|
|
414
|
+
if (resolved instanceof OmpErrors) {
|
|
415
|
+
if (errors) errors.append(resolved);
|
|
416
|
+
else errors = resolved;
|
|
417
|
+
} else {
|
|
418
|
+
out[p.key] = resolved;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
out[p.key] = materializeDefault(payload);
|
|
422
|
+
}
|
|
297
423
|
continue;
|
|
298
424
|
}
|
|
299
425
|
if (p.opt || p.hasDefault) continue;
|
|
300
426
|
path.push(p.key);
|
|
301
|
-
const
|
|
427
|
+
const error = fail(path, expectedOf(p.val), MISSING);
|
|
302
428
|
path.pop();
|
|
303
|
-
|
|
429
|
+
if (errors) errors.append(error);
|
|
430
|
+
else errors = error;
|
|
431
|
+
continue;
|
|
304
432
|
}
|
|
305
433
|
path.push(p.key);
|
|
306
|
-
const
|
|
434
|
+
const result = visit(p.val, rec[p.key], path);
|
|
307
435
|
path.pop();
|
|
308
|
-
if (
|
|
309
|
-
|
|
436
|
+
if (result instanceof OmpErrors) {
|
|
437
|
+
if (errors) errors.append(result);
|
|
438
|
+
else errors = result;
|
|
439
|
+
} else if (out) {
|
|
440
|
+
out[p.key] = result;
|
|
441
|
+
}
|
|
310
442
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
443
|
+
for (const key in rec) {
|
|
444
|
+
if (!own.call(rec, key)) continue;
|
|
445
|
+
let indexed = false;
|
|
446
|
+
if (ir.index !== undefined) {
|
|
447
|
+
indexed = true;
|
|
314
448
|
path.push(key);
|
|
315
|
-
const
|
|
449
|
+
const result = visit(ir.index, rec[key], path);
|
|
316
450
|
path.pop();
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!own.call(rec, key)) continue;
|
|
323
|
-
let declared = false;
|
|
324
|
-
for (const p of ir.props) {
|
|
325
|
-
if (p.key === key) {
|
|
326
|
-
declared = true;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
451
|
+
if (result instanceof OmpErrors) {
|
|
452
|
+
if (errors) errors.append(result);
|
|
453
|
+
else errors = result;
|
|
454
|
+
} else if (out) {
|
|
455
|
+
out[key] = result;
|
|
329
456
|
}
|
|
330
|
-
|
|
457
|
+
}
|
|
458
|
+
if (ir.patternIndexes !== undefined) {
|
|
459
|
+
for (const pattern of ir.patternIndexes) {
|
|
460
|
+
if (!checks(pattern.key, key)) continue;
|
|
461
|
+
indexed = true;
|
|
331
462
|
path.push(key);
|
|
332
|
-
const
|
|
463
|
+
const result = visit(pattern.val, rec[key], path);
|
|
333
464
|
path.pop();
|
|
334
|
-
|
|
465
|
+
if (result instanceof OmpErrors) {
|
|
466
|
+
if (errors) errors.append(result);
|
|
467
|
+
else errors = result;
|
|
468
|
+
} else if (out) {
|
|
469
|
+
out[key] = result;
|
|
470
|
+
}
|
|
335
471
|
}
|
|
336
472
|
}
|
|
473
|
+
if (ir.extras === "reject" && !indexed && !ir.props.some(prop => prop.key === key)) {
|
|
474
|
+
path.push(key);
|
|
475
|
+
const error = fail(path, "removed", rec[key]);
|
|
476
|
+
path.pop();
|
|
477
|
+
if (errors) errors.append(error);
|
|
478
|
+
else errors = error;
|
|
479
|
+
}
|
|
337
480
|
}
|
|
338
|
-
|
|
481
|
+
for (const key of Object.getOwnPropertySymbols(rec)) {
|
|
482
|
+
if (!Object.prototype.propertyIsEnumerable.call(rec, key)) continue;
|
|
483
|
+
if (ir.symbolIndex !== undefined) {
|
|
484
|
+
path.push(key);
|
|
485
|
+
const result = visit(ir.symbolIndex, rec[key], path);
|
|
486
|
+
path.pop();
|
|
487
|
+
if (result instanceof OmpErrors) {
|
|
488
|
+
if (errors) errors.append(result);
|
|
489
|
+
else errors = result;
|
|
490
|
+
} else if (out) {
|
|
491
|
+
out[key] = result;
|
|
492
|
+
}
|
|
493
|
+
} else if (ir.extras === "reject" && !ir.props.some(prop => prop.key === key)) {
|
|
494
|
+
path.push(key);
|
|
495
|
+
const error = fail(path, "removed", rec[key]);
|
|
496
|
+
path.pop();
|
|
497
|
+
if (errors) errors.append(error);
|
|
498
|
+
else errors = error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return errors ?? out ?? v;
|
|
502
|
+
}
|
|
503
|
+
case "string": {
|
|
504
|
+
if (typeof v !== "string") return fail(path, "a string", v);
|
|
505
|
+
if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v.length);
|
|
506
|
+
if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v.length);
|
|
507
|
+
if (ir.url && !URL.canParse(v)) return fail(path, "a URL string", v);
|
|
508
|
+
return v;
|
|
509
|
+
}
|
|
510
|
+
case "number": {
|
|
511
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return fail(path, ir.int ? "an integer" : "a number", v);
|
|
512
|
+
let errors: OmpErrors | undefined;
|
|
513
|
+
const add = (expected: string): void => {
|
|
514
|
+
const error = fail(path, expected, v);
|
|
515
|
+
if (errors) errors.append(error);
|
|
516
|
+
else errors = error;
|
|
517
|
+
};
|
|
518
|
+
if (ir.int && !Number.isInteger(v)) add("an integer");
|
|
519
|
+
if (ir.divisor !== undefined && v % ir.divisor !== 0) add(`a number divisible by ${ir.divisor}`);
|
|
520
|
+
if (ir.min !== undefined && (ir.xmin ? v <= ir.min : v < ir.min)) {
|
|
521
|
+
add(
|
|
522
|
+
ir.min === 0
|
|
523
|
+
? ir.xmin
|
|
524
|
+
? "positive"
|
|
525
|
+
: "non-negative"
|
|
526
|
+
: `a number ${ir.xmin ? "more than" : "at least"} ${ir.min}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (ir.max !== undefined && (ir.xmax ? v >= ir.max : v > ir.max)) {
|
|
530
|
+
add(
|
|
531
|
+
ir.max === 0
|
|
532
|
+
? ir.xmax
|
|
533
|
+
? "negative"
|
|
534
|
+
: "non-positive"
|
|
535
|
+
: `a number ${ir.xmax ? "less than" : "at most"} ${ir.max}`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return errors ?? v;
|
|
539
|
+
}
|
|
540
|
+
case "lit": {
|
|
541
|
+
if (checks(ir, v)) return v;
|
|
542
|
+
if ((typeof ir.v === "object" && ir.v !== null) || typeof ir.v === "function") {
|
|
543
|
+
let expected = "the specified reference";
|
|
544
|
+
try {
|
|
545
|
+
const serialized = JSON.stringify(ir.v);
|
|
546
|
+
if (serialized !== undefined) {
|
|
547
|
+
expected = `reference equal to ${serialized}`;
|
|
548
|
+
if (typeof v === "object" && v !== null && JSON.stringify(v) === serialized) {
|
|
549
|
+
expected += " (serialized to the same value)";
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
// Cyclic values still get a useful reference-identity expectation.
|
|
554
|
+
}
|
|
555
|
+
return fail(path, expected, v);
|
|
556
|
+
}
|
|
557
|
+
return fail(path, expectedOf(ir), v);
|
|
339
558
|
}
|
|
340
559
|
default:
|
|
341
560
|
return checks(ir, v) ? v : fail(path, expectedOf(ir), v);
|
|
@@ -349,16 +568,7 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
|
|
|
349
568
|
|
|
350
569
|
/** True when a union failure can be replaced with a more specific nested error. */
|
|
351
570
|
export function canRefineUnionFailure(member: IR): boolean {
|
|
352
|
-
const base = member
|
|
353
|
-
if (member.k === "sub") {
|
|
354
|
-
return (
|
|
355
|
-
base.k === "array" ||
|
|
356
|
-
base.k === "object" ||
|
|
357
|
-
base.k === "anyobject" ||
|
|
358
|
-
base.k === "string" ||
|
|
359
|
-
base.k === "number"
|
|
360
|
-
);
|
|
361
|
-
}
|
|
571
|
+
const base = unwrapBase(member);
|
|
362
572
|
if (base.k === "array" || base.k === "object") return true;
|
|
363
573
|
if (base.k === "string") return base.min !== undefined || base.max !== undefined || base.url === true;
|
|
364
574
|
return base.k === "number" && (base.int === true || base.min !== undefined || base.max !== undefined);
|
|
@@ -373,39 +583,168 @@ export function canRefineUnionFailure(member: IR): boolean {
|
|
|
373
583
|
*/
|
|
374
584
|
export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[], expected?: string): OmpErrors {
|
|
375
585
|
let best: IR | undefined;
|
|
376
|
-
for (const
|
|
377
|
-
const base =
|
|
586
|
+
for (const member of ir.members) {
|
|
587
|
+
const base = unwrapBase(member);
|
|
378
588
|
if (!kindMatches(base, v)) continue;
|
|
379
589
|
if (best !== undefined) {
|
|
380
590
|
best = undefined;
|
|
381
591
|
break;
|
|
382
592
|
}
|
|
383
|
-
best =
|
|
593
|
+
best = member;
|
|
594
|
+
}
|
|
595
|
+
if (best === undefined) {
|
|
596
|
+
const discriminated = discriminateFailure(ir.members, v, path);
|
|
597
|
+
if (discriminated !== undefined) return discriminated;
|
|
384
598
|
}
|
|
385
|
-
if (best
|
|
386
|
-
if (best) {
|
|
599
|
+
if (best !== undefined) {
|
|
387
600
|
const out = visit(best, v, path);
|
|
388
601
|
if (out instanceof OmpErrors) return out;
|
|
389
602
|
}
|
|
603
|
+
if (ir.members.every(member => unwrapBase(member).k === "object")) {
|
|
604
|
+
const branches = ir.members.flatMap(member => {
|
|
605
|
+
const result = visit(member, v, path);
|
|
606
|
+
return result instanceof OmpErrors ? [[...result]] : [];
|
|
607
|
+
});
|
|
608
|
+
if (branches.length !== 0) {
|
|
609
|
+
const common = branches[0].filter(
|
|
610
|
+
(entry, index, first) =>
|
|
611
|
+
first.findIndex(candidate => pathsEqual(candidate.path, entry.path)) === index &&
|
|
612
|
+
branches.every(branch => branch.some(candidate => pathsEqual(candidate.path, entry.path))),
|
|
613
|
+
);
|
|
614
|
+
const alternatives: OmpErrors[] = [];
|
|
615
|
+
if (common.length !== 0) {
|
|
616
|
+
for (const entry of common) {
|
|
617
|
+
const expectations = new Set<string>();
|
|
618
|
+
for (const branch of branches) {
|
|
619
|
+
for (const candidate of branch) {
|
|
620
|
+
if (pathsEqual(candidate.path, entry.path)) {
|
|
621
|
+
expectations.add(candidate.expected.endsWith(" instance") ? "an object" : candidate.expected);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
alternatives.push(
|
|
626
|
+
new OmpErrors(entry.path, [...expectations].join(" or "), entry.data, { preserveActual: true }),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
for (const branch of branches) {
|
|
631
|
+
for (const entry of branch) {
|
|
632
|
+
alternatives.push(
|
|
633
|
+
new OmpErrors(
|
|
634
|
+
entry.path,
|
|
635
|
+
entry.expected.endsWith(" instance") ? "an object" : entry.expected,
|
|
636
|
+
entry.data,
|
|
637
|
+
{ preserveActual: true },
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const combined = alternatives[0];
|
|
644
|
+
for (let index = 1; index < alternatives.length; index++) combined.append(alternatives[index]);
|
|
645
|
+
return alternatives.length === 1 ? combined : combined.asAlternatives();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
390
648
|
return fail(path, expected ?? expectedOf(ir), v);
|
|
391
649
|
}
|
|
392
650
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
651
|
+
interface LiteralDiscriminant {
|
|
652
|
+
path: PropertyKey[];
|
|
653
|
+
value: unknown;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function unwrapBase(member: IR, seen = new Set<IR>()): IR {
|
|
657
|
+
if (seen.has(member)) return member;
|
|
658
|
+
seen.add(member);
|
|
659
|
+
if (member.k === "sub") return unwrapBase(member.schema.ir, seen);
|
|
660
|
+
if (member.k === "alias") return unwrapBase(member.resolve(), seen);
|
|
661
|
+
if (member.k === "refine") return unwrapBase(member.base, seen);
|
|
662
|
+
return member;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function collectDiscriminants(member: IR, prefix: PropertyKey[] = [], seen = new Set<IR>()): LiteralDiscriminant[] {
|
|
666
|
+
if (seen.has(member)) return [];
|
|
667
|
+
seen.add(member);
|
|
668
|
+
if (member.k === "alias") return collectDiscriminants(member.resolve(), prefix, seen);
|
|
669
|
+
if (member.k === "sub") return collectDiscriminants(member.schema.ir, prefix, seen);
|
|
670
|
+
if (member.k === "refine") return collectDiscriminants(member.base, prefix, seen);
|
|
671
|
+
if (member.k !== "object") return [];
|
|
672
|
+
const result: LiteralDiscriminant[] = [];
|
|
673
|
+
for (const property of member.props) {
|
|
674
|
+
const propertyPath = [...prefix, property.key];
|
|
675
|
+
const value = unwrapBase(property.val);
|
|
676
|
+
if (value.k === "lit") result.push({ path: propertyPath, value: value.v });
|
|
677
|
+
else result.push(...collectDiscriminants(property.val, propertyPath, new Set(seen)));
|
|
678
|
+
}
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function pathsEqual(left: readonly PropertyKey[], right: readonly PropertyKey[]): boolean {
|
|
683
|
+
return left.length === right.length && left.every((key, index) => key === right[index]);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function valueAtPath(value: unknown, path: readonly PropertyKey[]): { present: boolean; value?: unknown } {
|
|
687
|
+
let cursor = value;
|
|
688
|
+
for (const key of path) {
|
|
689
|
+
if ((typeof cursor !== "object" && typeof cursor !== "function") || cursor === null || !(key in cursor)) {
|
|
690
|
+
return { present: false };
|
|
691
|
+
}
|
|
692
|
+
cursor = (cursor as Record<PropertyKey, unknown>)[key];
|
|
693
|
+
}
|
|
694
|
+
return { present: true, value: cursor };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function discriminateFailure(members: IR[], value: unknown, path: PropertyKey[]): OmpErrors | undefined {
|
|
698
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
699
|
+
const byMember = members.map(member => collectDiscriminants(member));
|
|
700
|
+
const candidates: { path: PropertyKey[]; distinct: number; declared: number }[] = [];
|
|
701
|
+
for (const discriminants of byMember) {
|
|
702
|
+
for (const discriminant of discriminants) {
|
|
703
|
+
if (candidates.some(candidate => pathsEqual(candidate.path, discriminant.path))) continue;
|
|
704
|
+
const values: unknown[] = [];
|
|
705
|
+
let declared = 0;
|
|
706
|
+
for (const branch of byMember) {
|
|
707
|
+
const match = branch.find(candidate => pathsEqual(candidate.path, discriminant.path));
|
|
708
|
+
if (match === undefined) continue;
|
|
709
|
+
declared++;
|
|
710
|
+
if (!values.some(candidate => Object.is(candidate, match.value))) values.push(match.value);
|
|
711
|
+
}
|
|
712
|
+
if (values.length > 1) candidates.push({ path: discriminant.path, distinct: values.length, declared });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
candidates.sort((left, right) => right.distinct - left.distinct || right.declared - left.declared);
|
|
716
|
+
for (const candidate of candidates) {
|
|
717
|
+
const actual = valueAtPath(value, candidate.path);
|
|
718
|
+
const exact: IR[] = [];
|
|
719
|
+
const defaults: IR[] = [];
|
|
720
|
+
const expectedMembers: IR[] = [];
|
|
721
|
+
for (let index = 0; index < members.length; index++) {
|
|
722
|
+
const discriminant = byMember[index].find(item => pathsEqual(item.path, candidate.path));
|
|
723
|
+
if (discriminant === undefined) {
|
|
724
|
+
defaults.push(members[index]);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
expectedMembers.push({ k: "lit", v: discriminant.value });
|
|
728
|
+
if (actual.present && Object.is(actual.value, discriminant.value)) exact.push(members[index]);
|
|
729
|
+
}
|
|
730
|
+
if (!actual.present) {
|
|
731
|
+
if (defaults.length !== 0 && defaults.length < members.length) {
|
|
732
|
+
return discriminateFailure(defaults, value, path);
|
|
733
|
+
}
|
|
734
|
+
return fail([...path, ...candidate.path], expectedOf({ k: "union", members: expectedMembers }), undefined);
|
|
735
|
+
}
|
|
736
|
+
if (exact.length === 0) {
|
|
737
|
+
if (defaults.length !== 0) return discriminateFailure(defaults, value, path);
|
|
738
|
+
return fail([...path, ...candidate.path], expectedOf({ k: "union", members: expectedMembers }), actual.value);
|
|
739
|
+
}
|
|
740
|
+
if (exact.length === 1) {
|
|
741
|
+
const result = visit(exact[0], value, path);
|
|
742
|
+
return result instanceof OmpErrors ? result : undefined;
|
|
406
743
|
}
|
|
744
|
+
const nested = discriminateFailure(exact, value, path);
|
|
745
|
+
if (nested !== undefined) return nested;
|
|
407
746
|
}
|
|
408
|
-
return
|
|
747
|
+
return undefined;
|
|
409
748
|
}
|
|
410
749
|
|
|
411
750
|
/** True when a value's runtime shape could only be aimed at this member. */
|