@oh-my-pi/omptype 17.2.6 → 17.2.7
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 +17 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +747 -0
- package/dist/js/errors.js +199 -0
- package/dist/js/index.js +12 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +476 -0
- package/dist/js/ir.js +930 -0
- package/dist/js/json-schema.js +273 -0
- package/dist/js/keywords.js +233 -0
- package/dist/js/type.js +1078 -0
- package/dist/js/typebox.js +346 -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 +51 -15
- package/dist/types/index.d.ts +3 -4
- package/dist/types/infer.d.ts +107 -30
- package/dist/types/interp.d.ts +5 -3
- package/dist/types/ir.d.ts +76 -7
- package/dist/types/json-schema.d.ts +6 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +238 -49
- package/dist/types/zod.d.ts +1 -1
- package/package.json +20 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +432 -54
- package/src/errors.ts +146 -34
- package/src/index.ts +3 -4
- package/src/infer.ts +284 -77
- package/src/interp.ts +153 -9
- package/src/ir.ts +636 -89
- package/src/json-schema.ts +109 -14
- package/src/keywords.ts +270 -0
- package/src/type.ts +1302 -147
- package/src/typebox.ts +1 -1
- package/src/zod.ts +40 -27
package/dist/js/ir.js
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema IR and the ArkType-compatible definition parser.
|
|
3
|
+
*
|
|
4
|
+
* `parseDef` turns the definition subset this repo uses — string DSL
|
|
5
|
+
* (primitives, literals, unions, arrays, bounds, `number.integer`,
|
|
6
|
+
* `string.url`, inline `= literal` defaults), object literals (optional `?`
|
|
7
|
+
* keys, `"+"` undeclared-key policy, `"[string]"` index signatures), tuple
|
|
8
|
+
* `[def, "[]"]` arrays, and embedded `Type` instances — into a small IR tree
|
|
9
|
+
* consumed by the interpreter (`interp.ts`), the JIT compiler (`compile.ts`),
|
|
10
|
+
* and the JSON Schema emitter (`json-schema.ts`).
|
|
11
|
+
*/
|
|
12
|
+
import { OmpErrors, OmpTypeError } from "./errors.js";
|
|
13
|
+
import { keywordIR, patternIR, templateIR } from "./keywords.js";
|
|
14
|
+
/** Brand carried by `Type` instances so the parser can embed them in defs. */
|
|
15
|
+
export const IR_BRAND = Symbol("omptype.schema");
|
|
16
|
+
const kMorph = Symbol("omptype.hasMorph");
|
|
17
|
+
const SIMPLE_OPS = "|()[]=?%";
|
|
18
|
+
function tokenize(src) {
|
|
19
|
+
const toks = [];
|
|
20
|
+
let i = 0;
|
|
21
|
+
const n = src.length;
|
|
22
|
+
while (i < n) {
|
|
23
|
+
const c = src[i];
|
|
24
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (c === "'" || c === '"') {
|
|
29
|
+
let j = i + 1;
|
|
30
|
+
while (j < n && src[j] !== c)
|
|
31
|
+
j++;
|
|
32
|
+
if (j >= n)
|
|
33
|
+
throw new OmpTypeError(`unterminated string literal in "${src}"`);
|
|
34
|
+
toks.push({ t: "str", v: src.slice(i + 1, j) });
|
|
35
|
+
i = j + 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (c === "d" && (src[i + 1] === "'" || src[i + 1] === '"')) {
|
|
39
|
+
const quote = src[i + 1];
|
|
40
|
+
const end = src.indexOf(quote, i + 2);
|
|
41
|
+
if (end < 0)
|
|
42
|
+
throw new OmpTypeError(`unterminated date literal in "${src}"`);
|
|
43
|
+
const value = new Date(src.slice(i + 2, end));
|
|
44
|
+
if (Number.isNaN(value.valueOf()))
|
|
45
|
+
throw new OmpTypeError(`invalid date literal in "${src}"`);
|
|
46
|
+
toks.push({ t: "date", v: value });
|
|
47
|
+
i = end + 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if ((c >= "0" && c <= "9") || (c === "-" && i + 1 < n && src[i + 1] >= "0" && src[i + 1] <= "9")) {
|
|
51
|
+
let j = i + 1;
|
|
52
|
+
while (j < n && ((src[j] >= "0" && src[j] <= "9") || src[j] === "." || src[j] === "e" || src[j] === "+"))
|
|
53
|
+
j++;
|
|
54
|
+
toks.push({ t: "num", v: Number(src.slice(i, j)) });
|
|
55
|
+
i = j;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
59
|
+
let j = i + 1;
|
|
60
|
+
while (j < n && /[\w.$]/.test(src[j]))
|
|
61
|
+
j++;
|
|
62
|
+
toks.push({ t: "id", v: src.slice(i, j) });
|
|
63
|
+
i = j;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (c === "<" || c === ">") {
|
|
67
|
+
if (src[i + 1] === "=") {
|
|
68
|
+
toks.push({ t: "op", v: `${c}=` });
|
|
69
|
+
i += 2;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
toks.push({ t: "op", v: c });
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (SIMPLE_OPS.includes(c)) {
|
|
78
|
+
toks.push({ t: "op", v: c });
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw new OmpTypeError(`unexpected character '${c}' in "${src}"`);
|
|
83
|
+
}
|
|
84
|
+
return toks;
|
|
85
|
+
}
|
|
86
|
+
// ── string-definition parser ─────────────────────────────────────────────────
|
|
87
|
+
const CMP = { "<": true, "<=": true, ">": true, ">=": true };
|
|
88
|
+
const KEYWORDS = {
|
|
89
|
+
number: () => ({ k: "number" }),
|
|
90
|
+
"number.integer": () => ({ k: "number", int: true }),
|
|
91
|
+
boolean: () => ({ k: "boolean" }),
|
|
92
|
+
bigint: () => ({ k: "bigint" }),
|
|
93
|
+
symbol: () => ({ k: "symbol" }),
|
|
94
|
+
never: () => ({ k: "never" }),
|
|
95
|
+
null: () => ({ k: "null" }),
|
|
96
|
+
undefined: () => ({ k: "undefined" }),
|
|
97
|
+
unknown: () => ({ k: "unknown" }),
|
|
98
|
+
any: () => ({ k: "unknown" }),
|
|
99
|
+
object: () => ({ k: "anyobject" }),
|
|
100
|
+
Date: () => ({ k: "instance", ctor: Date, expected: "a Date" }),
|
|
101
|
+
true: () => ({ k: "lit", v: true }),
|
|
102
|
+
false: () => ({ k: "lit", v: false }),
|
|
103
|
+
};
|
|
104
|
+
class StrParser {
|
|
105
|
+
#toks;
|
|
106
|
+
#pos = 0;
|
|
107
|
+
#src;
|
|
108
|
+
#resolve;
|
|
109
|
+
constructor(src, resolve) {
|
|
110
|
+
this.#src = src;
|
|
111
|
+
this.#resolve = resolve;
|
|
112
|
+
this.#toks = tokenize(src);
|
|
113
|
+
}
|
|
114
|
+
#peek(offset = 0) {
|
|
115
|
+
return this.#toks[this.#pos + offset];
|
|
116
|
+
}
|
|
117
|
+
#next() {
|
|
118
|
+
const t = this.#toks[this.#pos++];
|
|
119
|
+
if (!t)
|
|
120
|
+
throw new OmpTypeError(`unexpected end of definition "${this.#src}"`);
|
|
121
|
+
return t;
|
|
122
|
+
}
|
|
123
|
+
#eatOp(v) {
|
|
124
|
+
const t = this.#peek();
|
|
125
|
+
if (t?.t === "op" && t.v === v) {
|
|
126
|
+
this.#pos++;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/** Full definition with optional trailing `= literal` default and/or `?` optional marker. */
|
|
132
|
+
parseTop() {
|
|
133
|
+
const ir = this.parseUnion();
|
|
134
|
+
let def;
|
|
135
|
+
let hasDefault = false;
|
|
136
|
+
if (this.#eatOp("=")) {
|
|
137
|
+
const t = this.#next();
|
|
138
|
+
if (t.t === "num" || t.t === "str")
|
|
139
|
+
def = t.v;
|
|
140
|
+
else if (t.t === "id" && (t.v === "true" || t.v === "false"))
|
|
141
|
+
def = t.v === "true";
|
|
142
|
+
else if (t.t === "id" && t.v === "null")
|
|
143
|
+
def = null;
|
|
144
|
+
else
|
|
145
|
+
throw new OmpTypeError(`unsupported default literal in "${this.#src}"`);
|
|
146
|
+
hasDefault = true;
|
|
147
|
+
}
|
|
148
|
+
const optional = this.#eatOp("?");
|
|
149
|
+
this.#expectEnd();
|
|
150
|
+
return { ir, def, hasDefault, optional };
|
|
151
|
+
}
|
|
152
|
+
#expectEnd() {
|
|
153
|
+
if (this.#pos < this.#toks.length) {
|
|
154
|
+
throw new OmpTypeError(`trailing tokens in definition "${this.#src}"`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
parseUnion() {
|
|
158
|
+
const first = this.parseBounded();
|
|
159
|
+
if (!this.#eatOp("|"))
|
|
160
|
+
return first;
|
|
161
|
+
const members = [first, this.parseBounded()];
|
|
162
|
+
while (this.#eatOp("|"))
|
|
163
|
+
members.push(this.parseBounded());
|
|
164
|
+
return { k: "union", members };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `NUM CMP base (CMP NUM)?` or `base (CMP NUM)?`, with `[]*` postfix on the
|
|
168
|
+
* base AND after a trailing bound — `string>0[]` is an array of bounded
|
|
169
|
+
* strings, matching ArkType precedence (bounds bind tighter than `[]`).
|
|
170
|
+
*/
|
|
171
|
+
parseBounded() {
|
|
172
|
+
const t = this.#peek();
|
|
173
|
+
const t1 = this.#peek(1);
|
|
174
|
+
if ((t?.t === "num" || t?.t === "date") && t1?.t === "op" && CMP[t1.v]) {
|
|
175
|
+
const lo = t.v;
|
|
176
|
+
this.#pos += 2;
|
|
177
|
+
let node = this.#eatDivisor(this.parsePostfix());
|
|
178
|
+
node = applyBound(node, flip(t1.v), lo, this.#src);
|
|
179
|
+
const t2 = this.#peek();
|
|
180
|
+
if (t2?.t === "op" && CMP[t2.v]) {
|
|
181
|
+
this.#pos++;
|
|
182
|
+
const hi = this.#next();
|
|
183
|
+
if (hi.t !== "num" && hi.t !== "date") {
|
|
184
|
+
throw new OmpTypeError(`expected bound after comparator in "${this.#src}"`);
|
|
185
|
+
}
|
|
186
|
+
node = applyBound(node, t2.v, hi.v, this.#src);
|
|
187
|
+
}
|
|
188
|
+
return this.#eatArraySuffixes(node);
|
|
189
|
+
}
|
|
190
|
+
let node = this.#eatDivisor(this.parsePostfix());
|
|
191
|
+
const t2 = this.#peek();
|
|
192
|
+
if (t2?.t === "op" && CMP[t2.v]) {
|
|
193
|
+
this.#pos++;
|
|
194
|
+
const limit = this.#next();
|
|
195
|
+
if (limit.t !== "num" && limit.t !== "date") {
|
|
196
|
+
throw new OmpTypeError(`expected bound after comparator in "${this.#src}"`);
|
|
197
|
+
}
|
|
198
|
+
node = applyBound(node, t2.v, limit.v, this.#src);
|
|
199
|
+
node = this.#eatArraySuffixes(node);
|
|
200
|
+
}
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
#eatDivisor(node) {
|
|
204
|
+
if (!this.#eatOp("%"))
|
|
205
|
+
return node;
|
|
206
|
+
const divisor = this.#next();
|
|
207
|
+
if (divisor.t !== "num")
|
|
208
|
+
throw new OmpTypeError(`expected number after % in "${this.#src}"`);
|
|
209
|
+
if (node.k !== "number")
|
|
210
|
+
throw new OmpTypeError(`% requires number in "${this.#src}"`);
|
|
211
|
+
if (!Number.isFinite(divisor.v) || divisor.v === 0)
|
|
212
|
+
throw new OmpTypeError(`divisor must be non-zero in "${this.#src}"`);
|
|
213
|
+
node.divisor = divisor.v;
|
|
214
|
+
return node;
|
|
215
|
+
}
|
|
216
|
+
/** Wrap `node` in array IR for each `[]` pair at the cursor. */
|
|
217
|
+
#eatArraySuffixes(node) {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const t = this.#peek();
|
|
220
|
+
if (!(t?.t === "op" && t.v === "["))
|
|
221
|
+
return node;
|
|
222
|
+
this.#pos++;
|
|
223
|
+
if (!this.#eatOp("]"))
|
|
224
|
+
throw new OmpTypeError(`expected ']' in "${this.#src}"`);
|
|
225
|
+
node = { k: "array", el: node };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
parsePostfix() {
|
|
229
|
+
let node = this.parsePrimary();
|
|
230
|
+
for (;;) {
|
|
231
|
+
const t = this.#peek();
|
|
232
|
+
if (!(t?.t === "op" && t.v === "["))
|
|
233
|
+
break;
|
|
234
|
+
this.#pos++;
|
|
235
|
+
if (!this.#eatOp("]"))
|
|
236
|
+
throw new OmpTypeError(`expected ']' in "${this.#src}"`);
|
|
237
|
+
node = { k: "array", el: node };
|
|
238
|
+
}
|
|
239
|
+
return node;
|
|
240
|
+
}
|
|
241
|
+
parsePrimary() {
|
|
242
|
+
const t = this.#next();
|
|
243
|
+
if (t.t === "op" && t.v === "(") {
|
|
244
|
+
const inner = this.parseUnion();
|
|
245
|
+
if (!this.#eatOp(")"))
|
|
246
|
+
throw new OmpTypeError(`expected ')' in "${this.#src}"`);
|
|
247
|
+
return inner;
|
|
248
|
+
}
|
|
249
|
+
if (t.t === "str" || t.t === "num" || t.t === "date")
|
|
250
|
+
return { k: "lit", v: t.v };
|
|
251
|
+
if (t.t === "id") {
|
|
252
|
+
const make = KEYWORDS[t.v];
|
|
253
|
+
const keyword = make?.() ?? keywordIR(t.v) ?? this.#resolve?.(t.v);
|
|
254
|
+
if (!keyword)
|
|
255
|
+
throw new OmpTypeError(`unknown keyword "${t.v}" in "${this.#src}"`);
|
|
256
|
+
return keyword;
|
|
257
|
+
}
|
|
258
|
+
throw new OmpTypeError(`unexpected token in "${this.#src}"`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const STRING_DEF_CACHE_MAX = 1_024;
|
|
262
|
+
const stringDefCache = new Map();
|
|
263
|
+
function isWhitespaceAt(src, index) {
|
|
264
|
+
const code = src.charCodeAt(index);
|
|
265
|
+
return code === 32 || (code >= 9 && code <= 13) || (code > 127 && /\s/.test(src[index]));
|
|
266
|
+
}
|
|
267
|
+
/** Fast path for the literal unions pervasive in command schemas. */
|
|
268
|
+
function parseLiteralUnion(src) {
|
|
269
|
+
const members = [];
|
|
270
|
+
let index = 0;
|
|
271
|
+
while (index < src.length && isWhitespaceAt(src, index))
|
|
272
|
+
index++;
|
|
273
|
+
for (;;) {
|
|
274
|
+
const quote = src[index];
|
|
275
|
+
if (quote !== "'" && quote !== '"')
|
|
276
|
+
return undefined;
|
|
277
|
+
const end = src.indexOf(quote, index + 1);
|
|
278
|
+
if (end < 0)
|
|
279
|
+
return undefined;
|
|
280
|
+
members.push({ k: "lit", v: src.slice(index + 1, end) });
|
|
281
|
+
index = end + 1;
|
|
282
|
+
while (index < src.length && isWhitespaceAt(src, index))
|
|
283
|
+
index++;
|
|
284
|
+
if (index === src.length) {
|
|
285
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
286
|
+
}
|
|
287
|
+
if (src[index] !== "|")
|
|
288
|
+
return undefined;
|
|
289
|
+
index++;
|
|
290
|
+
while (index < src.length && isWhitespaceAt(src, index))
|
|
291
|
+
index++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function genericArguments(src) {
|
|
295
|
+
const open = src.indexOf("<");
|
|
296
|
+
if (open < 1 || !src.endsWith(">"))
|
|
297
|
+
return undefined;
|
|
298
|
+
const name = src.slice(0, open).trim();
|
|
299
|
+
const body = src.slice(open + 1, -1);
|
|
300
|
+
const args = [];
|
|
301
|
+
let depth = 0;
|
|
302
|
+
let quote = "";
|
|
303
|
+
let start = 0;
|
|
304
|
+
for (let index = 0; index < body.length; index++) {
|
|
305
|
+
const char = body[index];
|
|
306
|
+
if (quote !== "") {
|
|
307
|
+
if (char === quote && body[index - 1] !== "\\")
|
|
308
|
+
quote = "";
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
312
|
+
quote = char;
|
|
313
|
+
}
|
|
314
|
+
else if (char === "<" || char === "(" || char === "[") {
|
|
315
|
+
depth++;
|
|
316
|
+
}
|
|
317
|
+
else if (char === ">" || char === ")" || char === "]") {
|
|
318
|
+
depth--;
|
|
319
|
+
}
|
|
320
|
+
else if (char === "," && depth === 0) {
|
|
321
|
+
args.push(body.slice(start, index).trim());
|
|
322
|
+
start = index + 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
args.push(body.slice(start).trim());
|
|
326
|
+
return { name, args };
|
|
327
|
+
}
|
|
328
|
+
function genericKeys(ir) {
|
|
329
|
+
const keys = new Set();
|
|
330
|
+
const visit = (node) => {
|
|
331
|
+
if (node.k === "lit" && typeof node.v === "string")
|
|
332
|
+
keys.add(node.v);
|
|
333
|
+
else if (node.k === "union")
|
|
334
|
+
for (const member of node.members)
|
|
335
|
+
visit(member);
|
|
336
|
+
};
|
|
337
|
+
visit(ir);
|
|
338
|
+
return keys;
|
|
339
|
+
}
|
|
340
|
+
function resolveStructuralIR(ir) {
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
while (ir.k === "alias") {
|
|
343
|
+
if (seen.has(ir))
|
|
344
|
+
throw new OmpTypeError(`cannot structurally transform recursive alias "${ir.name}"`);
|
|
345
|
+
seen.add(ir);
|
|
346
|
+
ir = ir.resolve();
|
|
347
|
+
}
|
|
348
|
+
return ir;
|
|
349
|
+
}
|
|
350
|
+
function mergeObjectIR(left, right) {
|
|
351
|
+
left = resolveStructuralIR(left);
|
|
352
|
+
right = resolveStructuralIR(right);
|
|
353
|
+
if (left.k !== "object" || right.k !== "object") {
|
|
354
|
+
throw new OmpTypeError("Merge requires object arguments");
|
|
355
|
+
}
|
|
356
|
+
const props = [...left.props];
|
|
357
|
+
for (const prop of right.props) {
|
|
358
|
+
const index = props.findIndex(candidate => candidate.key === prop.key);
|
|
359
|
+
if (index < 0)
|
|
360
|
+
props.push(prop);
|
|
361
|
+
else
|
|
362
|
+
props[index] = prop;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
k: "object",
|
|
366
|
+
props,
|
|
367
|
+
index: right.index ?? left.index,
|
|
368
|
+
extras: right.extras === "keep" ? left.extras : right.extras,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function parseGeneric(src, resolve) {
|
|
372
|
+
const generic = genericArguments(src);
|
|
373
|
+
if (generic === undefined)
|
|
374
|
+
return undefined;
|
|
375
|
+
if (generic.name === "Record" && generic.args.length === 2) {
|
|
376
|
+
return { k: "object", props: [], index: parseDef(generic.args[1], resolve), extras: "keep" };
|
|
377
|
+
}
|
|
378
|
+
if ((generic.name === "Partial" || generic.name === "Required") && generic.args.length === 1) {
|
|
379
|
+
const object = resolveStructuralIR(parseDef(generic.args[0], resolve));
|
|
380
|
+
if (object.k !== "object")
|
|
381
|
+
throw new OmpTypeError(`${generic.name} requires an object`);
|
|
382
|
+
const optional = generic.name === "Partial";
|
|
383
|
+
return { ...object, props: object.props.map(prop => ({ ...prop, opt: optional })) };
|
|
384
|
+
}
|
|
385
|
+
if ((generic.name === "Pick" || generic.name === "Omit") && generic.args.length === 2) {
|
|
386
|
+
const object = resolveStructuralIR(parseDef(generic.args[0], resolve));
|
|
387
|
+
if (object.k !== "object")
|
|
388
|
+
throw new OmpTypeError(`${generic.name} requires an object`);
|
|
389
|
+
const keys = genericKeys(parseDef(generic.args[1], resolve));
|
|
390
|
+
const pick = generic.name === "Pick";
|
|
391
|
+
return { ...object, props: object.props.filter(prop => keys.has(prop.key) === pick) };
|
|
392
|
+
}
|
|
393
|
+
if (generic.name === "Merge" && generic.args.length === 2) {
|
|
394
|
+
return mergeObjectIR(parseDef(generic.args[0], resolve), parseDef(generic.args[1], resolve));
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
function dateLiteral(source) {
|
|
399
|
+
const value = new Date(source);
|
|
400
|
+
if (!Number.isFinite(value.valueOf()))
|
|
401
|
+
throw new OmpTypeError(`invalid Date literal "${source}"`);
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
function parseDateExpression(src) {
|
|
405
|
+
const first = src.charCodeAt(0);
|
|
406
|
+
if (first !== 68 && first !== 100)
|
|
407
|
+
return undefined;
|
|
408
|
+
const literal = src.match(/^d(['"])(.*)\1$/);
|
|
409
|
+
if (literal)
|
|
410
|
+
return { k: "lit", v: dateLiteral(literal[2]) };
|
|
411
|
+
const forward = src.match(/^Date\s*(<=|<|>=|>)\s*d(['"])(.*)\2$/);
|
|
412
|
+
const reverse = src.match(/^d(['"])(.*)\1\s*(<=|<|>=|>)\s*Date$/);
|
|
413
|
+
if (!forward && !reverse)
|
|
414
|
+
return undefined;
|
|
415
|
+
const bound = dateLiteral(forward ? forward[3] : (reverse?.[2] ?? ""));
|
|
416
|
+
const operator = forward
|
|
417
|
+
? forward[1]
|
|
418
|
+
: reverse?.[3] === "<="
|
|
419
|
+
? ">="
|
|
420
|
+
: reverse?.[3] === "<"
|
|
421
|
+
? ">"
|
|
422
|
+
: reverse?.[3] === ">="
|
|
423
|
+
? "<="
|
|
424
|
+
: "<";
|
|
425
|
+
const timestamp = bound.valueOf();
|
|
426
|
+
const relation = operator === ">="
|
|
427
|
+
? "at or after"
|
|
428
|
+
: operator === ">"
|
|
429
|
+
? "later than"
|
|
430
|
+
: operator === "<="
|
|
431
|
+
? "at or before"
|
|
432
|
+
: "earlier than";
|
|
433
|
+
return {
|
|
434
|
+
k: "refine",
|
|
435
|
+
base: { k: "instance", ctor: Date, expected: "a Date" },
|
|
436
|
+
pred: value => {
|
|
437
|
+
if (!(value instanceof Date))
|
|
438
|
+
return false;
|
|
439
|
+
const actual = value.valueOf();
|
|
440
|
+
return operator === ">="
|
|
441
|
+
? actual >= timestamp
|
|
442
|
+
: operator === ">"
|
|
443
|
+
? actual > timestamp
|
|
444
|
+
: operator === "<="
|
|
445
|
+
? actual <= timestamp
|
|
446
|
+
: actual < timestamp;
|
|
447
|
+
},
|
|
448
|
+
expected: `a Date ${relation} ${bound.toISOString()}`,
|
|
449
|
+
json: operator === ">=" || operator === ">" ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/** Parse recurring global DSL fragments once; scoped aliases bypass the cache. */
|
|
453
|
+
function parseStringDef(src, resolve) {
|
|
454
|
+
if (resolve === undefined) {
|
|
455
|
+
const cached = stringDefCache.get(src);
|
|
456
|
+
if (cached)
|
|
457
|
+
return cached;
|
|
458
|
+
}
|
|
459
|
+
let ir = parseDateExpression(src) ?? parseLiteralUnion(src) ?? parseGeneric(src, resolve);
|
|
460
|
+
if (ir === undefined && src.startsWith("`") && src.endsWith("`")) {
|
|
461
|
+
ir = templateIR(src.slice(1, -1));
|
|
462
|
+
}
|
|
463
|
+
else if (ir === undefined && src.startsWith("/") && src.lastIndexOf("/") > 0) {
|
|
464
|
+
const end = src.lastIndexOf("/");
|
|
465
|
+
try {
|
|
466
|
+
ir = patternIR(new RegExp(src.slice(1, end), src.slice(end + 1)));
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
throw new OmpTypeError(`invalid regular expression "${src}"`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const parsed = ir === undefined ? new StrParser(src, resolve).parseTop() : { ir, hasDefault: false, optional: false };
|
|
473
|
+
if (resolve === undefined && stringDefCache.size < STRING_DEF_CACHE_MAX)
|
|
474
|
+
stringDefCache.set(src, parsed);
|
|
475
|
+
return parsed;
|
|
476
|
+
}
|
|
477
|
+
function flip(op) {
|
|
478
|
+
switch (op) {
|
|
479
|
+
case "<":
|
|
480
|
+
return ">";
|
|
481
|
+
case "<=":
|
|
482
|
+
return ">=";
|
|
483
|
+
case ">":
|
|
484
|
+
return "<";
|
|
485
|
+
default:
|
|
486
|
+
return "<=";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** Apply `node CMP value` — numeric/string/array ranges or Date bounds. */
|
|
490
|
+
function applyBound(node, op, value, src) {
|
|
491
|
+
if (value instanceof Date) {
|
|
492
|
+
if (!acceptsDate(node))
|
|
493
|
+
throw new OmpTypeError(`date bound requires Date in "${src}"`);
|
|
494
|
+
const limit = value.valueOf();
|
|
495
|
+
const relation = op === ">=" ? "on or after" : op === ">" ? "later than" : op === "<=" ? "on or before" : "earlier than";
|
|
496
|
+
return {
|
|
497
|
+
k: "refine",
|
|
498
|
+
base: node,
|
|
499
|
+
pred: input => {
|
|
500
|
+
if (!(input instanceof Date))
|
|
501
|
+
return false;
|
|
502
|
+
const time = input.valueOf();
|
|
503
|
+
return op === ">=" ? time >= limit : op === ">" ? time > limit : op === "<=" ? time <= limit : time < limit;
|
|
504
|
+
},
|
|
505
|
+
expected: `a Date ${relation} ${value.toISOString()}`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (node.k === "number") {
|
|
509
|
+
switch (op) {
|
|
510
|
+
case ">=":
|
|
511
|
+
node.min = value;
|
|
512
|
+
node.xmin = false;
|
|
513
|
+
break;
|
|
514
|
+
case ">":
|
|
515
|
+
node.min = value;
|
|
516
|
+
node.xmin = true;
|
|
517
|
+
break;
|
|
518
|
+
case "<=":
|
|
519
|
+
node.max = value;
|
|
520
|
+
node.xmax = false;
|
|
521
|
+
break;
|
|
522
|
+
case "<":
|
|
523
|
+
node.max = value;
|
|
524
|
+
node.xmax = true;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
return node;
|
|
528
|
+
}
|
|
529
|
+
if (node.k === "string" || node.k === "array") {
|
|
530
|
+
switch (op) {
|
|
531
|
+
case ">=":
|
|
532
|
+
node.min = value;
|
|
533
|
+
break;
|
|
534
|
+
case ">":
|
|
535
|
+
node.min = value + 1;
|
|
536
|
+
break;
|
|
537
|
+
case "<=":
|
|
538
|
+
node.max = value;
|
|
539
|
+
break;
|
|
540
|
+
case "<":
|
|
541
|
+
node.max = value - 1;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
return node;
|
|
545
|
+
}
|
|
546
|
+
throw new OmpTypeError(`cannot bound ${node.k} in "${src}"`);
|
|
547
|
+
}
|
|
548
|
+
function acceptsDate(node) {
|
|
549
|
+
return (node.k === "instance" && node.ctor === Date) || (node.k === "refine" && acceptsDate(node.base));
|
|
550
|
+
}
|
|
551
|
+
// ── definition parser ────────────────────────────────────────────────────────
|
|
552
|
+
function isEmbedded(def) {
|
|
553
|
+
return (typeof def === "function" || (typeof def === "object" && def !== null)) && IR_BRAND in def;
|
|
554
|
+
}
|
|
555
|
+
/** Embed a schema value: inline pure structure, keep `sub` nodes for stepped schemas. */
|
|
556
|
+
export function embed(schema) {
|
|
557
|
+
if (schema.hasSteps)
|
|
558
|
+
return { k: "sub", schema, desc: schema.description };
|
|
559
|
+
if (schema.description !== undefined && schema.ir.desc === undefined) {
|
|
560
|
+
return { ...schema.ir, desc: schema.description };
|
|
561
|
+
}
|
|
562
|
+
return schema.ir;
|
|
563
|
+
}
|
|
564
|
+
function isCallback(value) {
|
|
565
|
+
return typeof value === "function";
|
|
566
|
+
}
|
|
567
|
+
function isConstructor(value) {
|
|
568
|
+
return typeof value === "function" && value.prototype !== undefined;
|
|
569
|
+
}
|
|
570
|
+
function parseTupleItem(def, resolve) {
|
|
571
|
+
if (Array.isArray(def) && def.length === 2 && def[1] === "?") {
|
|
572
|
+
return { val: parseDef(def[0], resolve), opt: true };
|
|
573
|
+
}
|
|
574
|
+
if (Array.isArray(def) && def.length === 3 && def[1] === "=") {
|
|
575
|
+
return {
|
|
576
|
+
val: parseDef(def[0], resolve),
|
|
577
|
+
opt: true,
|
|
578
|
+
def: def[2],
|
|
579
|
+
defFactory: typeof def[2] === "function",
|
|
580
|
+
hasDefault: true,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return { val: parseDef(def, resolve), opt: false };
|
|
584
|
+
}
|
|
585
|
+
function parseTuple(def, resolve) {
|
|
586
|
+
const prefix = [];
|
|
587
|
+
const postfix = [];
|
|
588
|
+
let variadic;
|
|
589
|
+
let optionalSeen = false;
|
|
590
|
+
for (let index = 0; index < def.length; index++) {
|
|
591
|
+
if (def[index] === "...") {
|
|
592
|
+
if (variadic !== undefined || index + 1 >= def.length) {
|
|
593
|
+
throw new OmpTypeError("a tuple may have one spread followed by an array definition");
|
|
594
|
+
}
|
|
595
|
+
const spread = parseDef(def[++index], resolve);
|
|
596
|
+
if (spread.k !== "array")
|
|
597
|
+
throw new OmpTypeError("tuple spread element must be an array");
|
|
598
|
+
variadic = spread.el;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (variadic !== undefined) {
|
|
602
|
+
const item = parseTupleItem(def[index], resolve);
|
|
603
|
+
if (item.opt || item.hasDefault) {
|
|
604
|
+
throw new OmpTypeError("optional tuple elements cannot follow a variadic element");
|
|
605
|
+
}
|
|
606
|
+
postfix.push(item.val);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const item = parseTupleItem(def[index], resolve);
|
|
610
|
+
if (optionalSeen && !item.opt && !item.hasDefault) {
|
|
611
|
+
throw new OmpTypeError("required tuple elements cannot follow optional elements");
|
|
612
|
+
}
|
|
613
|
+
optionalSeen ||= item.opt || item.hasDefault === true;
|
|
614
|
+
prefix.push(item);
|
|
615
|
+
}
|
|
616
|
+
return { k: "tuple", prefix, variadic, postfix };
|
|
617
|
+
}
|
|
618
|
+
/** Build the runtime schema for an object's or tuple's keys. */
|
|
619
|
+
export function keyOf(node) {
|
|
620
|
+
if (node.k === "object") {
|
|
621
|
+
const members = node.props.map(prop => ({ k: "lit", v: prop.key }));
|
|
622
|
+
if (node.index !== undefined)
|
|
623
|
+
members.push({ k: "string" });
|
|
624
|
+
if (members.length === 0)
|
|
625
|
+
return { k: "never" };
|
|
626
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
627
|
+
}
|
|
628
|
+
if (node.k === "tuple")
|
|
629
|
+
return { k: "number", int: true, min: 0 };
|
|
630
|
+
throw new OmpTypeError(`keyof requires an object or tuple (was ${node.k})`);
|
|
631
|
+
}
|
|
632
|
+
function parseArrayExpression(def, resolve) {
|
|
633
|
+
if (def.length === 2 && def[1] === "[]")
|
|
634
|
+
return { k: "array", el: parseDef(def[0], resolve) };
|
|
635
|
+
if (def.length === 2 && def[0] === "keyof")
|
|
636
|
+
return keyOf(parseDef(def[1], resolve));
|
|
637
|
+
if (def[0] === "instanceof") {
|
|
638
|
+
const members = [];
|
|
639
|
+
for (let index = 1; index < def.length; index++) {
|
|
640
|
+
const ctor = def[index];
|
|
641
|
+
if (!isConstructor(ctor))
|
|
642
|
+
throw new OmpTypeError("instanceof operands must be constructors");
|
|
643
|
+
members.push({ k: "instance", ctor, expected: `an instance of ${ctor.name || "the constructor"}` });
|
|
644
|
+
}
|
|
645
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
646
|
+
}
|
|
647
|
+
if (def[0] === "===") {
|
|
648
|
+
const members = def.slice(1).map(value => ({ k: "lit", v: value }));
|
|
649
|
+
if (members.length === 0)
|
|
650
|
+
return { k: "never" };
|
|
651
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
652
|
+
}
|
|
653
|
+
if (def.length >= 3 && def[1] === "|") {
|
|
654
|
+
return { k: "union", members: [parseDef(def[0], resolve), parseDef(def[2], resolve)] };
|
|
655
|
+
}
|
|
656
|
+
if (def.length >= 3 && def[1] === "&") {
|
|
657
|
+
return { k: "intersection", members: [parseDef(def[0], resolve), parseDef(def[2], resolve)] };
|
|
658
|
+
}
|
|
659
|
+
if (def.length === 3 && def[1] === "=>") {
|
|
660
|
+
if (!isCallback(def[2]))
|
|
661
|
+
throw new OmpTypeError("morph operator requires a function");
|
|
662
|
+
return { k: "morph", input: parseDef(def[0], resolve), fn: def[2] };
|
|
663
|
+
}
|
|
664
|
+
if (def.length === 3 && def[1] === "|>") {
|
|
665
|
+
return { k: "morph", input: parseDef(def[0], resolve), fn: value => value, out: parseDef(def[2], resolve) };
|
|
666
|
+
}
|
|
667
|
+
if (def.length === 3 && def[1] === ":") {
|
|
668
|
+
if (!isCallback(def[2]))
|
|
669
|
+
throw new OmpTypeError("narrow operator requires a predicate");
|
|
670
|
+
const predicate = def[2];
|
|
671
|
+
return {
|
|
672
|
+
k: "refine",
|
|
673
|
+
base: parseDef(def[0], resolve),
|
|
674
|
+
pred: value => predicate(value, {
|
|
675
|
+
error: () => OmpErrors.single([], "the predicate", value),
|
|
676
|
+
reject: () => OmpErrors.single([], "the predicate", value),
|
|
677
|
+
}) === true,
|
|
678
|
+
expected: "a value satisfying the predicate",
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (def.length >= 3 && def[1] === "@") {
|
|
682
|
+
const base = parseDef(def[0], resolve);
|
|
683
|
+
const meta = def[2];
|
|
684
|
+
if (typeof meta === "string")
|
|
685
|
+
return { ...base, desc: meta };
|
|
686
|
+
if (typeof meta === "object" && meta !== null && "description" in meta && typeof meta.description === "string") {
|
|
687
|
+
return { ...base, desc: meta.description };
|
|
688
|
+
}
|
|
689
|
+
return base;
|
|
690
|
+
}
|
|
691
|
+
return parseTuple(def, resolve);
|
|
692
|
+
}
|
|
693
|
+
function isObjectDefinition(def) {
|
|
694
|
+
return (typeof def === "object" &&
|
|
695
|
+
def !== null &&
|
|
696
|
+
!Array.isArray(def) &&
|
|
697
|
+
!(def instanceof RegExp) &&
|
|
698
|
+
!(def instanceof Date));
|
|
699
|
+
}
|
|
700
|
+
function parseObjectDefinition(def, resolve) {
|
|
701
|
+
const props = [];
|
|
702
|
+
let index;
|
|
703
|
+
let extras = "keep";
|
|
704
|
+
for (const rawKey in def) {
|
|
705
|
+
const val = def[rawKey];
|
|
706
|
+
if (rawKey === "+") {
|
|
707
|
+
if (val === "reject" || val === "delete")
|
|
708
|
+
extras = val;
|
|
709
|
+
else if (val === "ignore")
|
|
710
|
+
extras = "keep";
|
|
711
|
+
else
|
|
712
|
+
throw new OmpTypeError(`bad "+" value ${String(val)}`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (rawKey === "...") {
|
|
716
|
+
const spread = parseDef(val, resolve);
|
|
717
|
+
if (spread.k !== "object")
|
|
718
|
+
throw new OmpTypeError("object spread must resolve to an object");
|
|
719
|
+
for (const prop of spread.props) {
|
|
720
|
+
const previous = props.findIndex(candidate => candidate.key === prop.key);
|
|
721
|
+
if (previous < 0)
|
|
722
|
+
props.push(prop);
|
|
723
|
+
else
|
|
724
|
+
props[previous] = prop;
|
|
725
|
+
}
|
|
726
|
+
index ??= spread.index;
|
|
727
|
+
if (spread.extras !== "keep")
|
|
728
|
+
extras = spread.extras;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (rawKey === "[string]") {
|
|
732
|
+
index = parseDef(val, resolve);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const opt = rawKey.charCodeAt(rawKey.length - 1) === 63;
|
|
736
|
+
const key = opt ? rawKey.slice(0, -1) : rawKey;
|
|
737
|
+
if (typeof val === "string") {
|
|
738
|
+
const parsed = parseStringDef(val, resolve);
|
|
739
|
+
const prop = { key, opt: opt || parsed.optional, val: parsed.ir };
|
|
740
|
+
if (parsed.hasDefault) {
|
|
741
|
+
prop.def = parsed.def;
|
|
742
|
+
prop.hasDefault = true;
|
|
743
|
+
}
|
|
744
|
+
props.push(prop);
|
|
745
|
+
}
|
|
746
|
+
else if (Array.isArray(val) && val.length === 2 && val[1] === "?") {
|
|
747
|
+
props.push({ key, opt: true, val: parseDef(val[0], resolve) });
|
|
748
|
+
}
|
|
749
|
+
else if (Array.isArray(val) && val.length === 3 && val[1] === "=") {
|
|
750
|
+
props.push({
|
|
751
|
+
key,
|
|
752
|
+
opt,
|
|
753
|
+
val: parseDef(val[0], resolve),
|
|
754
|
+
def: val[2],
|
|
755
|
+
defFactory: typeof val[2] === "function",
|
|
756
|
+
hasDefault: true,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
else if (isEmbedded(val)) {
|
|
760
|
+
if (val.hasDefault) {
|
|
761
|
+
props.push({
|
|
762
|
+
key,
|
|
763
|
+
opt,
|
|
764
|
+
val: embed(val),
|
|
765
|
+
def: val.defaultValue,
|
|
766
|
+
defFactory: typeof val.defaultValue === "function",
|
|
767
|
+
hasDefault: true,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
props.push({ key, opt, val: embed(val) });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
else if (isObjectDefinition(val)) {
|
|
775
|
+
props.push({ key, opt, val: parseObjectDefinition(val, resolve) });
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
props.push({ key, opt, val: parseDef(val, resolve) });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return { k: "object", props, index, extras };
|
|
782
|
+
}
|
|
783
|
+
/** Parse a definition, optionally resolving names from an enclosing scope. */
|
|
784
|
+
export function parseDef(def, resolve) {
|
|
785
|
+
if (typeof def === "string") {
|
|
786
|
+
const parsed = parseStringDef(def, resolve);
|
|
787
|
+
if (parsed.optional) {
|
|
788
|
+
throw new OmpTypeError(`optional "?" marker is only valid on object property values`);
|
|
789
|
+
}
|
|
790
|
+
return parsed.ir;
|
|
791
|
+
}
|
|
792
|
+
if (Array.isArray(def))
|
|
793
|
+
return parseArrayExpression(def, resolve);
|
|
794
|
+
if (def instanceof RegExp)
|
|
795
|
+
return patternIR(def);
|
|
796
|
+
if (def instanceof Date)
|
|
797
|
+
return { k: "lit", v: def };
|
|
798
|
+
if (isEmbedded(def))
|
|
799
|
+
return embed(def);
|
|
800
|
+
if (isObjectDefinition(def))
|
|
801
|
+
return parseObjectDefinition(def, resolve);
|
|
802
|
+
throw new OmpTypeError(`unsupported definition ${String(def)}`);
|
|
803
|
+
}
|
|
804
|
+
/** True when validating `ir` can produce an output different from its input. */
|
|
805
|
+
export function hasMorph(ir) {
|
|
806
|
+
const cached = ir[kMorph];
|
|
807
|
+
if (cached !== undefined)
|
|
808
|
+
return cached;
|
|
809
|
+
let result = false;
|
|
810
|
+
switch (ir.k) {
|
|
811
|
+
case "sub":
|
|
812
|
+
result = true;
|
|
813
|
+
break;
|
|
814
|
+
case "morph":
|
|
815
|
+
case "alias":
|
|
816
|
+
result = true;
|
|
817
|
+
break;
|
|
818
|
+
case "object":
|
|
819
|
+
result = ir.extras === "delete";
|
|
820
|
+
for (let i = 0; !result && i < ir.props.length; i++) {
|
|
821
|
+
const prop = ir.props[i];
|
|
822
|
+
result = prop.hasDefault === true || hasMorph(prop.val);
|
|
823
|
+
}
|
|
824
|
+
if (!result && ir.index !== undefined)
|
|
825
|
+
result = hasMorph(ir.index);
|
|
826
|
+
break;
|
|
827
|
+
case "array":
|
|
828
|
+
result = hasMorph(ir.el);
|
|
829
|
+
break;
|
|
830
|
+
case "union":
|
|
831
|
+
result = ir.members.some(hasMorph);
|
|
832
|
+
break;
|
|
833
|
+
case "intersection":
|
|
834
|
+
result = ir.members.some(hasMorph);
|
|
835
|
+
break;
|
|
836
|
+
case "refine":
|
|
837
|
+
result = hasMorph(ir.base);
|
|
838
|
+
break;
|
|
839
|
+
case "tuple":
|
|
840
|
+
result =
|
|
841
|
+
ir.prefix.some(item => item.hasDefault === true || hasMorph(item.val)) ||
|
|
842
|
+
(ir.variadic !== undefined && hasMorph(ir.variadic)) ||
|
|
843
|
+
ir.postfix.some(hasMorph);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
ir[kMorph] = result;
|
|
847
|
+
return result;
|
|
848
|
+
}
|
|
849
|
+
/** Human-readable expectation for error messages, e.g. `"a string"`. */
|
|
850
|
+
export function expectedOf(ir) {
|
|
851
|
+
switch (ir.k) {
|
|
852
|
+
case "unknown":
|
|
853
|
+
return "unknown";
|
|
854
|
+
case "null":
|
|
855
|
+
return "null";
|
|
856
|
+
case "undefined":
|
|
857
|
+
return "undefined";
|
|
858
|
+
case "boolean":
|
|
859
|
+
return "boolean";
|
|
860
|
+
case "bigint":
|
|
861
|
+
return "a bigint";
|
|
862
|
+
case "symbol":
|
|
863
|
+
return "a symbol";
|
|
864
|
+
case "never":
|
|
865
|
+
return "never";
|
|
866
|
+
case "anyobject":
|
|
867
|
+
return "an object";
|
|
868
|
+
case "string": {
|
|
869
|
+
let out = ir.url ? "a URL string" : "a string";
|
|
870
|
+
if (ir.min !== undefined && ir.max !== undefined)
|
|
871
|
+
out += ` (length ${ir.min} to ${ir.max})`;
|
|
872
|
+
else if (ir.min !== undefined)
|
|
873
|
+
out += ` (length at least ${ir.min})`;
|
|
874
|
+
else if (ir.max !== undefined)
|
|
875
|
+
out += ` (length at most ${ir.max})`;
|
|
876
|
+
return out;
|
|
877
|
+
}
|
|
878
|
+
case "number": {
|
|
879
|
+
let out = ir.int ? "an integer" : "a number";
|
|
880
|
+
if (ir.min !== undefined)
|
|
881
|
+
out += ` ${ir.xmin ? "more than" : "at least"} ${ir.min}`;
|
|
882
|
+
if (ir.max !== undefined)
|
|
883
|
+
out += `${ir.min !== undefined ? " and" : ""} ${ir.xmax ? "less than" : "at most"} ${ir.max}`;
|
|
884
|
+
if (ir.divisor !== undefined)
|
|
885
|
+
out += ` divisible by ${ir.divisor}`;
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
case "lit":
|
|
889
|
+
return ir.v instanceof Date
|
|
890
|
+
? `the date ${ir.v.toISOString()}`
|
|
891
|
+
: typeof ir.v === "string"
|
|
892
|
+
? JSON.stringify(ir.v)
|
|
893
|
+
: String(ir.v);
|
|
894
|
+
case "union": {
|
|
895
|
+
if (ir.members.length === 0)
|
|
896
|
+
return "";
|
|
897
|
+
const first = expectedOf(ir.members[0]);
|
|
898
|
+
if (ir.members.length === 1)
|
|
899
|
+
return first;
|
|
900
|
+
const second = expectedOf(ir.members[1]);
|
|
901
|
+
if (ir.members.length === 2)
|
|
902
|
+
return first === second ? first : `${first} or ${second}`;
|
|
903
|
+
const expectations = first === second ? [first] : [first, second];
|
|
904
|
+
for (let i = 2; i < ir.members.length; i++) {
|
|
905
|
+
const expected = expectedOf(ir.members[i]);
|
|
906
|
+
if (!expectations.includes(expected))
|
|
907
|
+
expectations.push(expected);
|
|
908
|
+
}
|
|
909
|
+
return expectations.join(" or ");
|
|
910
|
+
}
|
|
911
|
+
case "intersection":
|
|
912
|
+
return ir.members.map(expectedOf).join(" and ");
|
|
913
|
+
case "array":
|
|
914
|
+
return "an array";
|
|
915
|
+
case "tuple":
|
|
916
|
+
return "a tuple";
|
|
917
|
+
case "object":
|
|
918
|
+
return "an object";
|
|
919
|
+
case "instance":
|
|
920
|
+
return ir.expected;
|
|
921
|
+
case "refine":
|
|
922
|
+
return ir.expected;
|
|
923
|
+
case "morph":
|
|
924
|
+
return expectedOf(ir.input);
|
|
925
|
+
case "alias":
|
|
926
|
+
return ir.name;
|
|
927
|
+
case "sub":
|
|
928
|
+
return expectedOf(ir.schema.ir);
|
|
929
|
+
}
|
|
930
|
+
}
|