@oh-my-pi/omptype 17.2.6
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 +8 -0
- package/README.md +60 -0
- package/dist/types/ark.d.ts +26 -0
- package/dist/types/compile.d.ts +5 -0
- package/dist/types/errors.d.ts +63 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/infer.d.ts +49 -0
- package/dist/types/interp.d.ts +24 -0
- package/dist/types/ir.d.ts +115 -0
- package/dist/types/json-schema.d.ts +8 -0
- package/dist/types/type.d.ts +100 -0
- package/dist/types/typebox.d.ts +184 -0
- package/dist/types/zod.d.ts +100 -0
- package/package.json +56 -0
- package/src/ark.ts +30 -0
- package/src/compile.ts +415 -0
- package/src/errors.ts +136 -0
- package/src/index.ts +13 -0
- package/src/infer.ts +160 -0
- package/src/interp.ts +282 -0
- package/src/ir.ts +480 -0
- package/src/json-schema.ts +172 -0
- package/src/type.ts +329 -0
- package/src/typebox.ts +487 -0
- package/src/zod.ts +339 -0
package/src/compile.ts
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIT compiler: lowers schema IR into a specialized validator via
|
|
3
|
+
* `new Function`. Invoked by `type.ts` after a schema's third call; the
|
|
4
|
+
* interpreter (`interp.ts`) covers earlier calls so rarely-used schemas never
|
|
5
|
+
* pay codegen cost.
|
|
6
|
+
*
|
|
7
|
+
* Generated code philosophy:
|
|
8
|
+
* - success path is straight-line monomorphic JS with zero allocation; when
|
|
9
|
+
* the schema has no morphs the input value itself is returned
|
|
10
|
+
* - failure allocates a single `OmpErrors` (`E(path, expected, data)`); path
|
|
11
|
+
* arrays and messages are inline literals, so cost is one small allocation
|
|
12
|
+
* - morphing nodes (defaults, `"+": "delete"`, embedded stepped schemas)
|
|
13
|
+
* produce a fresh output object; pure subtrees below them stay check-only
|
|
14
|
+
* - union members that morph are validated by hoisted interpreter closures;
|
|
15
|
+
* pure members compile to inline predicates
|
|
16
|
+
*/
|
|
17
|
+
import { MISSING, OmpErrors } from "./errors";
|
|
18
|
+
import { unionFail, walk } from "./interp";
|
|
19
|
+
import { expectedOf, hasMorph, type IR, type PropIR } from "./ir";
|
|
20
|
+
|
|
21
|
+
const own = Object.prototype.hasOwnProperty;
|
|
22
|
+
|
|
23
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
24
|
+
|
|
25
|
+
function access(base: string, key: string): string {
|
|
26
|
+
return IDENT.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Inline-able literal, else undefined (caller hoists into the refs pool). */
|
|
30
|
+
function litSource(v: unknown): string | undefined {
|
|
31
|
+
if (v === null) return "null";
|
|
32
|
+
if (v === undefined) return "undefined";
|
|
33
|
+
switch (typeof v) {
|
|
34
|
+
case "string":
|
|
35
|
+
case "boolean":
|
|
36
|
+
return JSON.stringify(v);
|
|
37
|
+
case "number":
|
|
38
|
+
return Number.isFinite(v) ? String(v) : undefined;
|
|
39
|
+
default:
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type PathSeg = { s: PropertyKey } | { d: string };
|
|
45
|
+
|
|
46
|
+
class Builder {
|
|
47
|
+
#lines: string[] = [];
|
|
48
|
+
#refs: unknown[] = [];
|
|
49
|
+
#id = 0;
|
|
50
|
+
|
|
51
|
+
next(prefix: string): string {
|
|
52
|
+
return `${prefix}${this.#id++}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
push(line: string): void {
|
|
56
|
+
this.#lines.push(line);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ref(value: unknown): string {
|
|
60
|
+
const idx = this.#refs.indexOf(value);
|
|
61
|
+
if (idx >= 0) return `R[${idx}]`;
|
|
62
|
+
this.#refs.push(value);
|
|
63
|
+
return `R[${this.#refs.length - 1}]`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lit(v: unknown): string {
|
|
67
|
+
return litSource(v) ?? this.ref(v);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pathExpr(segs: PathSeg[]): string {
|
|
71
|
+
const parts = segs.map(seg => ("s" in seg ? JSON.stringify(seg.s) : seg.d));
|
|
72
|
+
return `[${parts.join(",")}]`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fail(segs: PathSeg[], expected: string, dataExpr: string): string {
|
|
76
|
+
return `return E(${this.pathExpr(segs)},${JSON.stringify(expected)},${dataExpr})`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Pure boolean predicate for a morph-free subtree. */
|
|
80
|
+
predicate(node: IR, v: string): string {
|
|
81
|
+
switch (node.k) {
|
|
82
|
+
case "unknown":
|
|
83
|
+
return "true";
|
|
84
|
+
case "null":
|
|
85
|
+
return `${v}===null`;
|
|
86
|
+
case "undefined":
|
|
87
|
+
return `${v}===undefined`;
|
|
88
|
+
case "boolean":
|
|
89
|
+
return `typeof ${v}==="boolean"`;
|
|
90
|
+
case "bigint":
|
|
91
|
+
return `typeof ${v}==="bigint"`;
|
|
92
|
+
case "anyobject":
|
|
93
|
+
return `(typeof ${v}==="object"&&${v}!==null)`;
|
|
94
|
+
case "lit":
|
|
95
|
+
return `${v}===${this.lit(node.v)}`;
|
|
96
|
+
case "string": {
|
|
97
|
+
let out = `typeof ${v}==="string"`;
|
|
98
|
+
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
99
|
+
if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
|
|
100
|
+
if (node.url) out += `&&URL.canParse(${v})`;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
case "number": {
|
|
104
|
+
let out = `typeof ${v}==="number"`;
|
|
105
|
+
if (node.int) out += `&&Number.isInteger(${v})`;
|
|
106
|
+
if (node.min !== undefined) out += `&&${v}${node.xmin ? ">" : ">="}${node.min}`;
|
|
107
|
+
if (node.max !== undefined) out += `&&${v}${node.xmax ? "<" : "<="}${node.max}`;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
case "union": {
|
|
111
|
+
const lits = node.members.filter(m => m.k === "lit");
|
|
112
|
+
if (lits.length > 4) {
|
|
113
|
+
const set = this.ref(new Set(lits.map(m => m.v)));
|
|
114
|
+
const rest = node.members.filter(m => m.k !== "lit");
|
|
115
|
+
let out = `${set}.has(${v})`;
|
|
116
|
+
for (const m of rest) out += `||(${this.predicate(m, v)})`;
|
|
117
|
+
return `(${out})`;
|
|
118
|
+
}
|
|
119
|
+
return `(${node.members.map(m => `(${this.predicate(m, v)})`).join("||")})`;
|
|
120
|
+
}
|
|
121
|
+
case "array": {
|
|
122
|
+
const x = this.next("x");
|
|
123
|
+
let out = `Array.isArray(${v})`;
|
|
124
|
+
if (node.min !== undefined) out += `&&${v}.length>=${node.min}`;
|
|
125
|
+
if (node.max !== undefined) out += `&&${v}.length<=${node.max}`;
|
|
126
|
+
out += `&&${v}.every(${x}=>${this.predicate(node.el, x)})`;
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
case "object": {
|
|
130
|
+
const checks = [`typeof ${v}==="object"`, `${v}!==null`, `!Array.isArray(${v})`];
|
|
131
|
+
for (const p of node.props) {
|
|
132
|
+
const av = access(v, p.key);
|
|
133
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
134
|
+
checks.push(
|
|
135
|
+
p.opt || p.hasDefault
|
|
136
|
+
? `(!(${present})||(${this.predicate(p.val, av)}))`
|
|
137
|
+
: `((${present})&&(${this.predicate(p.val, av)}))`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (node.index) {
|
|
141
|
+
const k = this.next("k");
|
|
142
|
+
checks.push(
|
|
143
|
+
`(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.predicate(node.index, `${v}[${k}]`)}))return false;return true})()`,
|
|
144
|
+
);
|
|
145
|
+
} else if (node.extras === "reject") {
|
|
146
|
+
const k = this.next("k");
|
|
147
|
+
checks.push(
|
|
148
|
+
`(()=>{for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))return false;return true})()`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return `(${checks.join("&&")})`;
|
|
152
|
+
}
|
|
153
|
+
case "sub":
|
|
154
|
+
return `!(${this.ref(node.schema.run)}(${v}) instanceof AE)`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
declaredCheck(props: PropIR[], keyVar: string): string {
|
|
159
|
+
if (props.length === 0) return "false";
|
|
160
|
+
if (props.length > 6) {
|
|
161
|
+
const set = this.ref(new Set(props.map(p => p.key)));
|
|
162
|
+
return `${set}.has(${keyVar})`;
|
|
163
|
+
}
|
|
164
|
+
return `(${props.map(p => `${keyVar}===${JSON.stringify(p.key)}`).join("||")})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Statement-form check for a morph-free subtree with precise error paths. */
|
|
168
|
+
emitCheck(node: IR, v: string, segs: PathSeg[]): void {
|
|
169
|
+
switch (node.k) {
|
|
170
|
+
case "unknown":
|
|
171
|
+
return;
|
|
172
|
+
case "array": {
|
|
173
|
+
let head = `Array.isArray(${v})`;
|
|
174
|
+
if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
|
|
175
|
+
if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
|
|
176
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
|
|
177
|
+
const i = this.next("i");
|
|
178
|
+
const x = this.next("x");
|
|
179
|
+
this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];`);
|
|
180
|
+
this.emitCheck(node.el, x, [...segs, { d: i }]);
|
|
181
|
+
this.push("}");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
case "object": {
|
|
185
|
+
this.push(
|
|
186
|
+
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
|
|
187
|
+
);
|
|
188
|
+
for (const p of node.props) {
|
|
189
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
190
|
+
const propSegs: PathSeg[] = [...segs, { s: p.key }];
|
|
191
|
+
if (p.opt || p.hasDefault) {
|
|
192
|
+
this.push(`if(${present}){`);
|
|
193
|
+
this.emitCheck(p.val, access(v, p.key), propSegs);
|
|
194
|
+
this.push("}");
|
|
195
|
+
} else {
|
|
196
|
+
this.push(`if(!(${present}))${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
197
|
+
this.emitCheck(p.val, access(v, p.key), propSegs);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (node.index) {
|
|
201
|
+
const k = this.next("k");
|
|
202
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
|
|
203
|
+
this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
|
|
204
|
+
this.push("}");
|
|
205
|
+
} else if (node.extras === "reject") {
|
|
206
|
+
const k = this.next("k");
|
|
207
|
+
this.push(
|
|
208
|
+
`for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
|
|
209
|
+
[...segs, { d: k }],
|
|
210
|
+
"removed (undeclared key)",
|
|
211
|
+
`${v}[${k}]`,
|
|
212
|
+
)};`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
case "sub": {
|
|
218
|
+
const r = this.next("r");
|
|
219
|
+
this.push(`const ${r}=${this.ref(node.schema.run)}(${v});`);
|
|
220
|
+
this.push(`if(${r} instanceof AE)return PF(${r},${this.pathExpr(segs)});`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
case "union":
|
|
224
|
+
this.push(`if(!(${this.predicate(node, v)}))return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
|
|
225
|
+
return;
|
|
226
|
+
default:
|
|
227
|
+
this.push(`if(!(${this.predicate(node, v)}))${this.fail(segs, expectedOf(node), v)};`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate `v` against a morphing subtree and assign the produced output
|
|
233
|
+
* to `out` (an already-declared `let`).
|
|
234
|
+
*/
|
|
235
|
+
emitProduce(node: IR, v: string, segs: PathSeg[], out: string): void {
|
|
236
|
+
if (!hasMorph(node)) {
|
|
237
|
+
this.emitCheck(node, v, segs);
|
|
238
|
+
this.push(`${out}=${v};`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
switch (node.k) {
|
|
242
|
+
case "sub": {
|
|
243
|
+
const r = this.next("r");
|
|
244
|
+
this.push(`const ${r}=${this.ref(node.schema.run)}(${v});`);
|
|
245
|
+
this.push(`if(${r} instanceof AE)return PF(${r},${this.pathExpr(segs)});`);
|
|
246
|
+
this.push(`${out}=${r};`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case "union": {
|
|
250
|
+
const ok = this.next("u");
|
|
251
|
+
this.push(`let ${ok}=false;`);
|
|
252
|
+
const label = this.next("b");
|
|
253
|
+
this.push(`${label}:{`);
|
|
254
|
+
for (const m of node.members) {
|
|
255
|
+
if (m.k !== "sub" && !hasMorph(m)) {
|
|
256
|
+
this.push(`if(${this.predicate(m, v)}){${out}=${v};${ok}=true;break ${label};}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const m of node.members) {
|
|
260
|
+
if (m.k === "sub" || hasMorph(m)) {
|
|
261
|
+
const runner = m.k === "sub" ? m.schema.run : boundWalk(m);
|
|
262
|
+
const r = this.next("r");
|
|
263
|
+
this.push(`const ${r}=${this.ref(runner)}(${v});`);
|
|
264
|
+
this.push(`if(!(${r} instanceof AE)){${out}=${r};${ok}=true;break ${label};}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.push("}");
|
|
268
|
+
this.push(`if(!${ok})return UF(${this.ref(node)},${v},${this.pathExpr(segs)});`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
case "array": {
|
|
272
|
+
let head = `Array.isArray(${v})`;
|
|
273
|
+
if (node.min !== undefined) head += `&&${v}.length>=${node.min}`;
|
|
274
|
+
if (node.max !== undefined) head += `&&${v}.length<=${node.max}`;
|
|
275
|
+
this.push(`if(!(${head}))${this.fail(segs, expectedOf(node), v)};`);
|
|
276
|
+
const arr = this.next("a");
|
|
277
|
+
const i = this.next("i");
|
|
278
|
+
const x = this.next("x");
|
|
279
|
+
const el = this.next("t");
|
|
280
|
+
this.push(`const ${arr}=new Array(${v}.length);`);
|
|
281
|
+
this.push(`for(let ${i}=0;${i}<${v}.length;${i}++){const ${x}=${v}[${i}];let ${el};`);
|
|
282
|
+
this.emitProduce(node.el, x, [...segs, { d: i }], el);
|
|
283
|
+
this.push(`${arr}[${i}]=${el};}`);
|
|
284
|
+
this.push(`${out}=${arr};`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
case "object": {
|
|
288
|
+
this.push(
|
|
289
|
+
`if(typeof ${v}!=="object"||${v}===null||Array.isArray(${v}))${this.fail(segs, "an object", v)};`,
|
|
290
|
+
);
|
|
291
|
+
const o = this.next("o");
|
|
292
|
+
const fresh = node.extras === "delete" && !node.index;
|
|
293
|
+
this.push(fresh ? `const ${o}={};` : `const ${o}={...${v}};`);
|
|
294
|
+
for (const p of node.props) {
|
|
295
|
+
const present = `${JSON.stringify(p.key)} in ${v}`;
|
|
296
|
+
const propSegs: PathSeg[] = [...segs, { s: p.key }];
|
|
297
|
+
const av = access(v, p.key);
|
|
298
|
+
const ao = access(o, p.key);
|
|
299
|
+
const morphChild = hasMorph(p.val);
|
|
300
|
+
const missing: string[] = [];
|
|
301
|
+
if (p.hasDefault) {
|
|
302
|
+
const dflt = p.defFactory ? `${this.ref(p.def)}()` : this.lit(p.def);
|
|
303
|
+
missing.push(`${ao}=${dflt};`);
|
|
304
|
+
} else if (!p.opt) {
|
|
305
|
+
missing.push(`${this.fail(propSegs, expectedOf(p.val), "M")};`);
|
|
306
|
+
} else if (fresh) {
|
|
307
|
+
// optional + absent under "delete": nothing to copy
|
|
308
|
+
}
|
|
309
|
+
this.push(`if(!(${present})){${missing.join("")}}else{`);
|
|
310
|
+
if (morphChild) {
|
|
311
|
+
const t = this.next("t");
|
|
312
|
+
this.push(`let ${t};`);
|
|
313
|
+
this.emitProduce(p.val, av, propSegs, t);
|
|
314
|
+
this.push(`${ao}=${t};`);
|
|
315
|
+
} else {
|
|
316
|
+
this.emitCheck(p.val, av, propSegs);
|
|
317
|
+
if (fresh) this.push(`${ao}=${av};`);
|
|
318
|
+
}
|
|
319
|
+
this.push("}");
|
|
320
|
+
}
|
|
321
|
+
if (node.index) {
|
|
322
|
+
const k = this.next("k");
|
|
323
|
+
this.push(`for(const ${k} in ${v})if(own.call(${v},${k})){`);
|
|
324
|
+
if (hasMorph(node.index)) {
|
|
325
|
+
const t = this.next("t");
|
|
326
|
+
this.push(`let ${t};`);
|
|
327
|
+
this.emitProduce(node.index, `${v}[${k}]`, [...segs, { d: k }], t);
|
|
328
|
+
this.push(`${o}[${k}]=${t};`);
|
|
329
|
+
} else {
|
|
330
|
+
this.emitCheck(node.index, `${v}[${k}]`, [...segs, { d: k }]);
|
|
331
|
+
}
|
|
332
|
+
this.push("}");
|
|
333
|
+
} else if (node.extras === "reject") {
|
|
334
|
+
const k = this.next("k");
|
|
335
|
+
this.push(
|
|
336
|
+
`for(const ${k} in ${v})if(own.call(${v},${k})&&!(${this.declaredCheck(node.props, k)}))${this.fail(
|
|
337
|
+
[...segs, { d: k }],
|
|
338
|
+
"removed (undeclared key)",
|
|
339
|
+
`${v}[${k}]`,
|
|
340
|
+
)};`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
this.push(`${out}=${o};`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
default:
|
|
347
|
+
// morph-free kinds are handled by the hasMorph guard above
|
|
348
|
+
this.emitCheck(node, v, segs);
|
|
349
|
+
this.push(`${out}=${v};`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
build(ir: IR): (value: unknown) => unknown {
|
|
354
|
+
let ret: string;
|
|
355
|
+
if (hasMorph(ir)) {
|
|
356
|
+
this.push("let o;");
|
|
357
|
+
// body emitted below needs `o` declared first, so splice ordering:
|
|
358
|
+
this.emitProduce(ir, "v", [], "o");
|
|
359
|
+
ret = "o";
|
|
360
|
+
} else {
|
|
361
|
+
this.emitCheck(ir, "v", []);
|
|
362
|
+
ret = "v";
|
|
363
|
+
}
|
|
364
|
+
const src = `return function(v){${this.#lines.join("")}return ${ret}}`;
|
|
365
|
+
const make = new Function("R", "E", "AE", "M", "PF", "UF", "own", src) as (
|
|
366
|
+
refs: unknown[],
|
|
367
|
+
e: typeof OmpErrors.single,
|
|
368
|
+
ae: typeof OmpErrors,
|
|
369
|
+
m: typeof MISSING,
|
|
370
|
+
pf: typeof prefixErrors,
|
|
371
|
+
uf: typeof unionFail,
|
|
372
|
+
ownFn: typeof own,
|
|
373
|
+
) => (value: unknown) => unknown;
|
|
374
|
+
return make(this.#refs, OmpErrors.single, OmpErrors, MISSING, prefixErrors, unionFail, own);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function prefixErrors(errs: OmpErrors, parts: PropertyKey[]): OmpErrors {
|
|
379
|
+
for (let i = parts.length - 1; i >= 0; i--) errs.prefix(parts[i]);
|
|
380
|
+
return errs;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const kWalk = Symbol("omptype.boundWalk");
|
|
384
|
+
|
|
385
|
+
interface WalkTagged {
|
|
386
|
+
[kWalk]?: (value: unknown) => unknown;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Interpreter closure for a morphing union member (rare; hoisted into refs). */
|
|
390
|
+
function boundWalk(node: IR): (value: unknown) => unknown {
|
|
391
|
+
const tagged = node as IR & WalkTagged;
|
|
392
|
+
let fn = tagged[kWalk];
|
|
393
|
+
if (!fn) {
|
|
394
|
+
fn = (value: unknown) => walk(node, value);
|
|
395
|
+
tagged[kWalk] = fn;
|
|
396
|
+
}
|
|
397
|
+
return fn;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Compile `ir` into a specialized validator. */
|
|
401
|
+
export function compile(ir: IR): (value: unknown) => unknown {
|
|
402
|
+
return new Builder().build(ir);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Generated source for inspection/debugging. */
|
|
406
|
+
export function compileToSource(ir: IR): string {
|
|
407
|
+
const b = new Builder();
|
|
408
|
+
if (hasMorph(ir)) {
|
|
409
|
+
b.push("let o;");
|
|
410
|
+
b.emitProduce(ir, "v", [], "o");
|
|
411
|
+
return `function(v){/* refs elided */return o}`;
|
|
412
|
+
}
|
|
413
|
+
b.emitCheck(ir, "v", []);
|
|
414
|
+
return `function(v){/* refs elided */return v}`;
|
|
415
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation error containers mirroring ArkType's observable error surface:
|
|
3
|
+
* `result instanceof type.errors` / `instanceof OmpErrors`, lazy `.summary`,
|
|
4
|
+
* array iteration, and per-entry `.path` / `.problem` / `.message`.
|
|
5
|
+
*
|
|
6
|
+
* Failure-path cost matters: schemas reject untrusted input constantly, so
|
|
7
|
+
* construction stores only the path, the expectation, and the offending value.
|
|
8
|
+
* All human-readable strings are built lazily on property access.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A single validation failure at one path. */
|
|
12
|
+
export class OmpError {
|
|
13
|
+
constructor(
|
|
14
|
+
/** Property path from the root to the failing value (empty at root). */
|
|
15
|
+
readonly path: PropertyKey[],
|
|
16
|
+
/** Human-readable expectation, e.g. `"a string"` or `"at most 3600"`. */
|
|
17
|
+
readonly expected: string,
|
|
18
|
+
/** The value that failed validation. */
|
|
19
|
+
readonly data: unknown,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/** Short description of the received value, e.g. `"a number"` or `"missing"`. */
|
|
23
|
+
get actual(): string {
|
|
24
|
+
return describeValue(this.data);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Path-less problem statement: `must be <expected> (was <actual>)`. */
|
|
28
|
+
get problem(): string {
|
|
29
|
+
return this.data === MISSING
|
|
30
|
+
? `must be ${this.expected} (was missing)`
|
|
31
|
+
: `must be ${this.expected} (was ${this.actual})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Full message including the path prefix. */
|
|
35
|
+
get message(): string {
|
|
36
|
+
const at = this.path.length === 0 ? "" : `${this.path.map(String).join(".")} `;
|
|
37
|
+
return `${at}${this.problem}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toString(): string {
|
|
41
|
+
return this.message;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Sentinel for a required key that was absent (distinguishes from `undefined`). */
|
|
46
|
+
export const MISSING: unique symbol = Symbol("omptype.missing");
|
|
47
|
+
|
|
48
|
+
function describeValue(data: unknown): string {
|
|
49
|
+
if (data === null) return "null";
|
|
50
|
+
if (Array.isArray(data)) return "an array";
|
|
51
|
+
switch (typeof data) {
|
|
52
|
+
case "string":
|
|
53
|
+
return data.length <= 40 ? JSON.stringify(data) : `a string (length ${data.length})`;
|
|
54
|
+
case "number":
|
|
55
|
+
return String(data);
|
|
56
|
+
case "bigint":
|
|
57
|
+
return `${data}n`;
|
|
58
|
+
case "boolean":
|
|
59
|
+
return String(data);
|
|
60
|
+
case "undefined":
|
|
61
|
+
return "undefined";
|
|
62
|
+
case "object":
|
|
63
|
+
return "an object";
|
|
64
|
+
case "function":
|
|
65
|
+
return "a function";
|
|
66
|
+
default:
|
|
67
|
+
return "a symbol";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Aggregate of validation failures; the value returned by a schema call on
|
|
73
|
+
* invalid input. Array-like so callers can `.map()` over entries.
|
|
74
|
+
*/
|
|
75
|
+
export class OmpErrors extends Array<OmpError> {
|
|
76
|
+
// `.map()`/`.filter()` on an errors instance should produce plain arrays,
|
|
77
|
+
// not attempt `new OmpErrors(n)`.
|
|
78
|
+
static override get [Symbol.species](): typeof Array {
|
|
79
|
+
return Array;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Single-failure constructor used by generated validators. */
|
|
83
|
+
static single(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
|
|
84
|
+
const out = new OmpErrors();
|
|
85
|
+
out.push(new OmpError(path, expected, data));
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Prefix every entry's path with `key` (used when nesting sub-schemas). */
|
|
90
|
+
prefix(key: PropertyKey): this {
|
|
91
|
+
for (let i = 0; i < this.length; i++) {
|
|
92
|
+
const e = this[i];
|
|
93
|
+
this[i] = new OmpError([key, ...e.path], e.expected, e.data);
|
|
94
|
+
}
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Human-readable digest of every failure, one per line. Built lazily. */
|
|
99
|
+
get summary(): string {
|
|
100
|
+
let out = "";
|
|
101
|
+
for (let i = 0; i < this.length; i++) {
|
|
102
|
+
if (i > 0) out += "\n";
|
|
103
|
+
out += this[i].message;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override toString(): string {
|
|
109
|
+
return this.summary;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Throw a `TraversalError` carrying the summary. */
|
|
113
|
+
throw(): never {
|
|
114
|
+
throw new TraversalError(this);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Error thrown by `Type.assert` on invalid input. */
|
|
119
|
+
export class TraversalError extends Error {
|
|
120
|
+
constructor(readonly errors: OmpErrors) {
|
|
121
|
+
super(errors.summary);
|
|
122
|
+
this.name = "TraversalError";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Definition/usage error thrown while building a schema — malformed string
|
|
128
|
+
* DSL, unsupported composition, or an illegal builder call. Distinct from
|
|
129
|
+
* validation failures, which are returned as {@link OmpErrors}.
|
|
130
|
+
*/
|
|
131
|
+
export class OmpTypeError extends Error {
|
|
132
|
+
constructor(message: string) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "OmpTypeError";
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* omptype — ArkType-compatible schema validation with a lazy JIT runtime.
|
|
3
|
+
*
|
|
4
|
+
* Drop-in for the arktype surface this repo uses:
|
|
5
|
+
* `type()`, `Type`, `type.errors` / `OmpErrors`, `type.enumerated()`,
|
|
6
|
+
* `.or/.and/.array/.pipe/.narrow/.describe/.default/.allows/.assert/.toJsonSchema`,
|
|
7
|
+
* plus `typeof schema.infer` static inference.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./errors";
|
|
10
|
+
export * from "./infer";
|
|
11
|
+
export * from "./ir";
|
|
12
|
+
export * from "./json-schema";
|
|
13
|
+
export * from "./type";
|