@oh-my-pi/omptype 17.2.6 → 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 +40 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +942 -0
- package/dist/js/errors.js +394 -0
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +13 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +858 -0
- package/dist/js/ir.js +1700 -0
- package/dist/js/json-schema.js +312 -0
- package/dist/js/keywords.js +340 -0
- package/dist/js/type.js +3062 -0
- package/dist/js/typebox.js +355 -0
- package/dist/js/zod.js +269 -0
- package/dist/types/ark.d.ts +3 -11
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/errors.d.ts +53 -16
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/infer.d.ts +147 -31
- package/dist/types/interp.d.ts +14 -5
- package/dist/types/ir.d.ts +129 -9
- package/dist/types/json-schema.d.ts +15 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +525 -60
- package/dist/types/typebox.d.ts +78 -50
- package/dist/types/zod.d.ts +1 -1
- package/package.json +24 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +887 -154
- package/src/errors.ts +347 -37
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +4 -4
- package/src/infer.ts +363 -81
- package/src/interp.ts +585 -102
- package/src/ir.ts +1444 -128
- package/src/json-schema.ts +162 -19
- package/src/keywords.ts +380 -0
- package/src/type.ts +3842 -198
- package/src/typebox.ts +142 -99
- package/src/zod.ts +41 -28
package/src/interp.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tree-walking validator used for a schema's first few calls
|
|
3
|
-
*
|
|
2
|
+
* Tree-walking validator used for a schema's first few calls and as the
|
|
3
|
+
* targeted fallback for recursive or predicate-only JIT subtrees.
|
|
4
4
|
*
|
|
5
5
|
* Semantics must stay in lockstep with `compile.ts`:
|
|
6
6
|
* - success returns the output value; the input is returned as-is unless the
|
|
@@ -9,23 +9,65 @@
|
|
|
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 {
|
|
24
|
-
|
|
45
|
+
const storedPath = path.length === 0 ? undefined : path.length === 1 ? path[0] : [...path];
|
|
46
|
+
return new OmpErrors(storedPath, expected, data);
|
|
25
47
|
}
|
|
26
48
|
|
|
27
49
|
/** Pure predicate used for union-member scanning (no morphs, no errors). */
|
|
28
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 {
|
|
29
71
|
switch (ir.k) {
|
|
30
72
|
case "unknown":
|
|
31
73
|
return true;
|
|
@@ -37,6 +79,10 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
37
79
|
return typeof v === "boolean";
|
|
38
80
|
case "bigint":
|
|
39
81
|
return typeof v === "bigint";
|
|
82
|
+
case "symbol":
|
|
83
|
+
return typeof v === "symbol";
|
|
84
|
+
case "never":
|
|
85
|
+
return false;
|
|
40
86
|
case "anyobject":
|
|
41
87
|
return typeof v === "object" && v !== null;
|
|
42
88
|
case "string":
|
|
@@ -47,26 +93,54 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
47
93
|
(!ir.url || URL.canParse(v))
|
|
48
94
|
);
|
|
49
95
|
case "number":
|
|
96
|
+
if (typeof v !== "number") return false;
|
|
50
97
|
return (
|
|
51
|
-
|
|
52
|
-
(
|
|
98
|
+
(ir.int ? Number.isInteger(v) : Number.isFinite(v)) &&
|
|
99
|
+
(ir.divisor === undefined || v % ir.divisor === 0) &&
|
|
53
100
|
(ir.min === undefined || (ir.xmin ? v > ir.min : v >= ir.min)) &&
|
|
54
101
|
(ir.max === undefined || (ir.xmax ? v < ir.max : v <= ir.max))
|
|
55
102
|
);
|
|
56
103
|
case "lit":
|
|
57
|
-
return v === ir.v;
|
|
104
|
+
return ir.v instanceof Date ? v instanceof Date && v.valueOf() === ir.v.valueOf() : v === ir.v;
|
|
58
105
|
case "union":
|
|
59
106
|
return ir.members.some(m => checks(m, v));
|
|
107
|
+
case "intersection":
|
|
108
|
+
return ir.members.every(member => checks(member, v));
|
|
60
109
|
case "array": {
|
|
61
110
|
if (!Array.isArray(v)) return false;
|
|
62
111
|
if (ir.min !== undefined && v.length < ir.min) return false;
|
|
63
112
|
if (ir.max !== undefined && v.length > ir.max) return false;
|
|
64
|
-
for (const
|
|
113
|
+
for (const element of v) if (!checks(ir.el, element)) return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
case "tuple": {
|
|
117
|
+
if (!Array.isArray(v)) return false;
|
|
118
|
+
let required = ir.postfix.length;
|
|
119
|
+
for (const item of ir.prefix) if (!item.opt && !item.hasDefault) required++;
|
|
120
|
+
if (v.length < required) return false;
|
|
121
|
+
if (ir.variadic === undefined && v.length > ir.prefix.length + ir.postfix.length) return false;
|
|
122
|
+
const postfixStart = v.length - ir.postfix.length;
|
|
123
|
+
const prefixCount = Math.min(ir.prefix.length, postfixStart);
|
|
124
|
+
for (let index = 0; index < prefixCount; index++) {
|
|
125
|
+
if (!checks(ir.prefix[index].val, v[index])) return false;
|
|
126
|
+
}
|
|
127
|
+
for (let index = prefixCount; index < ir.prefix.length; index++) {
|
|
128
|
+
const item = ir.prefix[index];
|
|
129
|
+
if (!item.opt && !item.hasDefault) return false;
|
|
130
|
+
}
|
|
131
|
+
if (ir.variadic !== undefined) {
|
|
132
|
+
for (let index = prefixCount; index < postfixStart; index++) {
|
|
133
|
+
if (!checks(ir.variadic, v[index])) return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (let index = 0; index < ir.postfix.length; index++) {
|
|
137
|
+
if (!checks(ir.postfix[index], v[postfixStart + index])) return false;
|
|
138
|
+
}
|
|
65
139
|
return true;
|
|
66
140
|
}
|
|
67
141
|
case "object": {
|
|
68
|
-
if (typeof v !== "object" || v === null
|
|
69
|
-
const rec = v as Record<
|
|
142
|
+
if (typeof v !== "object" || v === null) return false;
|
|
143
|
+
const rec = v as Record<PropertyKey, unknown>;
|
|
70
144
|
for (const p of ir.props) {
|
|
71
145
|
const present = p.key in rec;
|
|
72
146
|
if (!present) {
|
|
@@ -75,34 +149,117 @@ function checks(ir: IR, v: unknown): boolean {
|
|
|
75
149
|
}
|
|
76
150
|
if (!checks(p.val, rec[p.key])) return false;
|
|
77
151
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (p.key === key) {
|
|
88
|
-
declared = true;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
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;
|
|
91
161
|
}
|
|
92
|
-
|
|
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;
|
|
93
177
|
}
|
|
94
178
|
}
|
|
95
179
|
return true;
|
|
96
180
|
}
|
|
181
|
+
case "instance":
|
|
182
|
+
return v instanceof ir.ctor;
|
|
183
|
+
case "refine":
|
|
184
|
+
if (!checks(ir.base, v)) return false;
|
|
185
|
+
try {
|
|
186
|
+
return ir.pred(v) === true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
case "alias":
|
|
191
|
+
return checks(ir.resolve(), v);
|
|
192
|
+
case "morph":
|
|
193
|
+
return checks(ir.input, v);
|
|
97
194
|
case "sub":
|
|
98
195
|
return !(ir.schema.run(v) instanceof OmpErrors);
|
|
99
196
|
}
|
|
100
197
|
}
|
|
101
198
|
|
|
102
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 {
|
|
103
228
|
switch (ir.k) {
|
|
229
|
+
case "alias":
|
|
230
|
+
return visit(ir.resolve(), v, path);
|
|
231
|
+
case "refine": {
|
|
232
|
+
const base = visit(ir.base, v, path);
|
|
233
|
+
if (base instanceof OmpErrors) return base;
|
|
234
|
+
try {
|
|
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);
|
|
238
|
+
} catch {
|
|
239
|
+
return fail(path, ir.expected, base);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
case "morph": {
|
|
243
|
+
const input = visit(ir.input, v, path);
|
|
244
|
+
if (input instanceof OmpErrors) return input;
|
|
245
|
+
const context = {
|
|
246
|
+
error: (expected: string, data: unknown = input) => fail(path, expected, data),
|
|
247
|
+
reject: (problem: string, data: unknown = input) => fail(path, problem, data),
|
|
248
|
+
};
|
|
249
|
+
const output = ir.fn(input, context);
|
|
250
|
+
if (output instanceof OmpErrors) return output;
|
|
251
|
+
return ir.out === undefined ? output : visit(ir.out, output, path);
|
|
252
|
+
}
|
|
253
|
+
case "intersection": {
|
|
254
|
+
let output = v;
|
|
255
|
+
for (const member of ir.members) {
|
|
256
|
+
output = visit(member, output, path);
|
|
257
|
+
if (output instanceof OmpErrors) return output;
|
|
258
|
+
}
|
|
259
|
+
return output;
|
|
260
|
+
}
|
|
104
261
|
case "sub": {
|
|
105
|
-
const out = ir.schema.run(v);
|
|
262
|
+
const out = ir.schema.run(v, path);
|
|
106
263
|
if (out instanceof OmpErrors) {
|
|
107
264
|
return path.length === 0 ? out : prefixAll(out, path);
|
|
108
265
|
}
|
|
@@ -116,46 +273,131 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
116
273
|
return v;
|
|
117
274
|
}
|
|
118
275
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
281
|
if (!(out instanceof OmpErrors)) return out;
|
|
282
|
+
if (kindMatches(unwrapBase(member), v)) {
|
|
283
|
+
targetCount++;
|
|
284
|
+
targeted ??= out;
|
|
285
|
+
}
|
|
123
286
|
}
|
|
124
287
|
}
|
|
125
|
-
|
|
288
|
+
if (targetCount === 1 && targeted !== undefined) return targeted;
|
|
289
|
+
return ir.members.some(canRefineUnionFailure) ? unionFail(ir, v, path) : fail(path, expectedOf(ir), v);
|
|
126
290
|
}
|
|
127
291
|
case "array": {
|
|
128
292
|
if (!Array.isArray(v)) return fail(path, "an array", v);
|
|
129
|
-
if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v);
|
|
130
|
-
if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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);
|
|
301
|
+
path.pop();
|
|
302
|
+
if (element instanceof OmpErrors) {
|
|
303
|
+
if (errors) errors.append(element);
|
|
304
|
+
else errors = element;
|
|
305
|
+
} else if (morph) {
|
|
306
|
+
out[index] = element;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return errors ?? out;
|
|
310
|
+
}
|
|
311
|
+
case "tuple": {
|
|
312
|
+
if (!Array.isArray(v)) return fail(path, "an array", v);
|
|
313
|
+
let required = ir.postfix.length;
|
|
314
|
+
for (const item of ir.prefix) if (!item.opt && !item.hasDefault) required++;
|
|
315
|
+
if (v.length < required) return fail(path, `an array of at least length ${required}`, v);
|
|
316
|
+
const maximum = ir.prefix.length + ir.postfix.length;
|
|
317
|
+
if (ir.variadic === undefined && v.length > maximum) {
|
|
318
|
+
return fail(path, `an array of at most length ${maximum}`, v);
|
|
319
|
+
}
|
|
320
|
+
const postfixStart = v.length - ir.postfix.length;
|
|
321
|
+
const prefixCount = Math.min(ir.prefix.length, postfixStart);
|
|
322
|
+
const morph = hasMorph(ir);
|
|
323
|
+
const output = morph ? [...v] : v;
|
|
324
|
+
let errors: OmpErrors | undefined;
|
|
325
|
+
for (let index = 0; index < prefixCount; index++) {
|
|
326
|
+
path.push(index);
|
|
327
|
+
const item = visit(ir.prefix[index].val, v[index], path);
|
|
328
|
+
path.pop();
|
|
329
|
+
if (item instanceof OmpErrors) {
|
|
330
|
+
if (errors) errors.append(item);
|
|
331
|
+
else errors = item;
|
|
332
|
+
} else if (morph) {
|
|
333
|
+
output[index] = item;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (let index = prefixCount; index < ir.prefix.length; index++) {
|
|
337
|
+
const item = ir.prefix[index];
|
|
338
|
+
if (item.hasDefault && morph) {
|
|
339
|
+
const payload = item.def;
|
|
340
|
+
if (item.defFactory && typeof payload === "function") {
|
|
341
|
+
path.push(index);
|
|
342
|
+
const resolved = visit(item.val, payload(), path);
|
|
136
343
|
path.pop();
|
|
137
|
-
|
|
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
|
+
}
|
|
353
|
+
} else if (!item.opt) {
|
|
354
|
+
path.push(index);
|
|
355
|
+
const error = fail(path, expectedOf(item.val), MISSING);
|
|
356
|
+
path.pop();
|
|
357
|
+
if (errors) errors.append(error);
|
|
358
|
+
else errors = error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (ir.variadic !== undefined) {
|
|
362
|
+
for (let index = prefixCount; index < postfixStart; index++) {
|
|
363
|
+
path.push(index);
|
|
364
|
+
const item = visit(ir.variadic, v[index], path);
|
|
365
|
+
path.pop();
|
|
366
|
+
if (item instanceof OmpErrors) {
|
|
367
|
+
if (errors) errors.append(item);
|
|
368
|
+
else errors = item;
|
|
369
|
+
} else if (morph) {
|
|
370
|
+
output[index] = item;
|
|
138
371
|
}
|
|
139
372
|
}
|
|
140
|
-
return v;
|
|
141
373
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
path.push(
|
|
145
|
-
const
|
|
374
|
+
for (let index = 0; index < ir.postfix.length; index++) {
|
|
375
|
+
const inputIndex = postfixStart + index;
|
|
376
|
+
path.push(inputIndex);
|
|
377
|
+
const item = visit(ir.postfix[index], v[inputIndex], path);
|
|
146
378
|
path.pop();
|
|
147
|
-
if (
|
|
148
|
-
|
|
379
|
+
if (item instanceof OmpErrors) {
|
|
380
|
+
if (errors) errors.append(item);
|
|
381
|
+
else errors = item;
|
|
382
|
+
} else if (morph) {
|
|
383
|
+
output[inputIndex] = item;
|
|
384
|
+
}
|
|
149
385
|
}
|
|
150
|
-
return
|
|
386
|
+
return errors ?? output;
|
|
151
387
|
}
|
|
152
388
|
case "object": {
|
|
153
|
-
if (typeof v !== "object" || v === null
|
|
154
|
-
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>;
|
|
155
391
|
const morph = hasMorph(ir);
|
|
156
|
-
let out: Record<
|
|
392
|
+
let out: Record<PropertyKey, unknown> | undefined;
|
|
393
|
+
let errors: OmpErrors | undefined;
|
|
157
394
|
if (morph) {
|
|
158
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
ir.extras === "delete" &&
|
|
397
|
+
ir.index === undefined &&
|
|
398
|
+
ir.symbolIndex === undefined &&
|
|
399
|
+
ir.patternIndexes === undefined
|
|
400
|
+
) {
|
|
159
401
|
out = {};
|
|
160
402
|
} else {
|
|
161
403
|
out = { ...rec };
|
|
@@ -164,51 +406,155 @@ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
|
|
|
164
406
|
for (const p of ir.props) {
|
|
165
407
|
if (!(p.key in rec)) {
|
|
166
408
|
if (p.hasDefault && out) {
|
|
167
|
-
// defFactory guarantees a callable default payload
|
|
168
409
|
const payload = p.def;
|
|
169
|
-
|
|
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
|
+
}
|
|
170
423
|
continue;
|
|
171
424
|
}
|
|
172
425
|
if (p.opt || p.hasDefault) continue;
|
|
173
426
|
path.push(p.key);
|
|
174
|
-
const
|
|
427
|
+
const error = fail(path, expectedOf(p.val), MISSING);
|
|
175
428
|
path.pop();
|
|
176
|
-
|
|
429
|
+
if (errors) errors.append(error);
|
|
430
|
+
else errors = error;
|
|
431
|
+
continue;
|
|
177
432
|
}
|
|
178
433
|
path.push(p.key);
|
|
179
|
-
const
|
|
434
|
+
const result = visit(p.val, rec[p.key], path);
|
|
180
435
|
path.pop();
|
|
181
|
-
if (
|
|
182
|
-
|
|
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
|
+
}
|
|
183
442
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
|
187
448
|
path.push(key);
|
|
188
|
-
const
|
|
449
|
+
const result = visit(ir.index, rec[key], path);
|
|
189
450
|
path.pop();
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!own.call(rec, key)) continue;
|
|
196
|
-
let declared = false;
|
|
197
|
-
for (const p of ir.props) {
|
|
198
|
-
if (p.key === key) {
|
|
199
|
-
declared = true;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
451
|
+
if (result instanceof OmpErrors) {
|
|
452
|
+
if (errors) errors.append(result);
|
|
453
|
+
else errors = result;
|
|
454
|
+
} else if (out) {
|
|
455
|
+
out[key] = result;
|
|
202
456
|
}
|
|
203
|
-
|
|
457
|
+
}
|
|
458
|
+
if (ir.patternIndexes !== undefined) {
|
|
459
|
+
for (const pattern of ir.patternIndexes) {
|
|
460
|
+
if (!checks(pattern.key, key)) continue;
|
|
461
|
+
indexed = true;
|
|
204
462
|
path.push(key);
|
|
205
|
-
const
|
|
463
|
+
const result = visit(pattern.val, rec[key], path);
|
|
206
464
|
path.pop();
|
|
207
|
-
|
|
465
|
+
if (result instanceof OmpErrors) {
|
|
466
|
+
if (errors) errors.append(result);
|
|
467
|
+
else errors = result;
|
|
468
|
+
} else if (out) {
|
|
469
|
+
out[key] = result;
|
|
470
|
+
}
|
|
208
471
|
}
|
|
209
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
|
+
}
|
|
210
480
|
}
|
|
211
|
-
|
|
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);
|
|
212
558
|
}
|
|
213
559
|
default:
|
|
214
560
|
return checks(ir, v) ? v : fail(path, expectedOf(ir), v);
|
|
@@ -220,6 +566,14 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
|
|
|
220
566
|
return errs;
|
|
221
567
|
}
|
|
222
568
|
|
|
569
|
+
/** True when a union failure can be replaced with a more specific nested error. */
|
|
570
|
+
export function canRefineUnionFailure(member: IR): boolean {
|
|
571
|
+
const base = unwrapBase(member);
|
|
572
|
+
if (base.k === "array" || base.k === "object") return true;
|
|
573
|
+
if (base.k === "string") return base.min !== undefined || base.max !== undefined || base.url === true;
|
|
574
|
+
return base.k === "number" && (base.int === true || base.min !== undefined || base.max !== undefined);
|
|
575
|
+
}
|
|
576
|
+
|
|
223
577
|
/**
|
|
224
578
|
* Detailed failure for a union: descend into the member the value was clearly
|
|
225
579
|
* aimed at — unique runtime-kind match, else an object member whose literal
|
|
@@ -227,41 +581,170 @@ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
|
|
|
227
581
|
* for a precise nested error (paths, narrow messages) instead of the coarse
|
|
228
582
|
* "A or B" expectation.
|
|
229
583
|
*/
|
|
230
|
-
export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[]): OmpErrors {
|
|
584
|
+
export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[], expected?: string): OmpErrors {
|
|
231
585
|
let best: IR | undefined;
|
|
232
|
-
for (const
|
|
233
|
-
const base =
|
|
586
|
+
for (const member of ir.members) {
|
|
587
|
+
const base = unwrapBase(member);
|
|
234
588
|
if (!kindMatches(base, v)) continue;
|
|
235
589
|
if (best !== undefined) {
|
|
236
590
|
best = undefined;
|
|
237
591
|
break;
|
|
238
592
|
}
|
|
239
|
-
best =
|
|
593
|
+
best = member;
|
|
594
|
+
}
|
|
595
|
+
if (best === undefined) {
|
|
596
|
+
const discriminated = discriminateFailure(ir.members, v, path);
|
|
597
|
+
if (discriminated !== undefined) return discriminated;
|
|
240
598
|
}
|
|
241
|
-
if (best
|
|
242
|
-
if (best) {
|
|
599
|
+
if (best !== undefined) {
|
|
243
600
|
const out = visit(best, v, path);
|
|
244
601
|
if (out instanceof OmpErrors) return out;
|
|
245
602
|
}
|
|
246
|
-
|
|
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
|
+
}
|
|
648
|
+
return fail(path, expected ?? expectedOf(ir), v);
|
|
247
649
|
}
|
|
248
650
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
743
|
}
|
|
744
|
+
const nested = discriminateFailure(exact, value, path);
|
|
745
|
+
if (nested !== undefined) return nested;
|
|
263
746
|
}
|
|
264
|
-
return
|
|
747
|
+
return undefined;
|
|
265
748
|
}
|
|
266
749
|
|
|
267
750
|
/** True when a value's runtime shape could only be aimed at this member. */
|