@oh-my-pi/omptype 17.2.7 → 17.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/js/compile.js +414 -219
- package/dist/js/errors.js +236 -41
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +1 -0
- package/dist/js/interp.js +514 -132
- package/dist/js/ir.js +972 -202
- package/dist/js/json-schema.js +73 -34
- package/dist/js/keywords.js +108 -1
- package/dist/js/type.js +2156 -172
- package/dist/js/typebox.js +41 -32
- package/dist/js/zod.js +2 -2
- package/dist/types/compile.d.ts +1 -1
- package/dist/types/errors.d.ts +19 -18
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/infer.d.ts +45 -6
- package/dist/types/interp.d.ts +9 -2
- package/dist/types/ir.d.ts +57 -6
- package/dist/types/json-schema.d.ts +10 -1
- package/dist/types/type.d.ts +324 -48
- package/dist/types/typebox.d.ts +78 -50
- package/package.json +5 -1
- package/src/compile.ts +598 -243
- package/src/errors.ts +247 -49
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +1 -0
- package/src/infer.ts +107 -32
- package/src/interp.ts +457 -118
- package/src/ir.ts +981 -212
- package/src/json-schema.ts +82 -34
- package/src/keywords.ts +111 -1
- package/src/type.ts +2760 -271
- package/src/typebox.ts +141 -98
- package/src/zod.ts +1 -1
package/dist/js/errors.js
CHANGED
|
@@ -26,37 +26,65 @@ export class OmpError {
|
|
|
26
26
|
this.#rawExpected = expected;
|
|
27
27
|
this.#config = config;
|
|
28
28
|
}
|
|
29
|
+
/** Prefix this failure when a nested schema delegates validation. */
|
|
30
|
+
prefix(key) {
|
|
31
|
+
this.path.unshift(key);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** Apply schema-local formatting to this failure. */
|
|
35
|
+
configure(config) {
|
|
36
|
+
this.#config = { ...this.#config, ...config };
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
29
39
|
/** Stable category for programmatic error handling. */
|
|
30
40
|
get code() {
|
|
31
41
|
return errorCode(this.#rawExpected, this.data);
|
|
32
42
|
}
|
|
33
43
|
#context(expected, actual, problem = "") {
|
|
34
|
-
|
|
44
|
+
const { description, rule } = describeExpectation(this.#rawExpected);
|
|
45
|
+
return {
|
|
46
|
+
code: this.code,
|
|
47
|
+
path: this.path,
|
|
48
|
+
propString: formatPath(this.path),
|
|
49
|
+
data: this.data,
|
|
50
|
+
expected,
|
|
51
|
+
actual,
|
|
52
|
+
problem,
|
|
53
|
+
description,
|
|
54
|
+
...(rule === undefined ? {} : { rule }),
|
|
55
|
+
};
|
|
35
56
|
}
|
|
36
57
|
/** Human-readable expectation, including a configured override. */
|
|
37
58
|
get expected() {
|
|
38
|
-
const actual = describeValue(this.data);
|
|
39
|
-
|
|
59
|
+
const actual = describeValue(this.data, this.#config?.preserveActual ? "predicate" : this.code);
|
|
60
|
+
const parts = this.#rawExpected.split(" or ");
|
|
61
|
+
const fallback = parts.length < 3 ? this.#rawExpected : `${parts.slice(0, -1).join(", ")} or ${parts[parts.length - 1]}`;
|
|
62
|
+
return format(this.#config?.expected, this.#context(fallback, actual), fallback);
|
|
40
63
|
}
|
|
41
64
|
/** Short description of the received value, e.g. `"a number"` or `"missing"`. */
|
|
42
65
|
get actual() {
|
|
43
|
-
const actual = describeValue(this.data);
|
|
44
|
-
|
|
66
|
+
const actual = describeValue(this.data, this.#config?.preserveActual ? "predicate" : this.code);
|
|
67
|
+
const override = this.#config?.actual;
|
|
68
|
+
return typeof override === "function" ? override(this.data) : (override ?? actual);
|
|
45
69
|
}
|
|
46
70
|
/** Path-less problem statement: `must be <expected> (was <actual>)`. */
|
|
47
71
|
get problem() {
|
|
48
72
|
const expected = this.expected;
|
|
49
73
|
const actual = this.actual;
|
|
50
|
-
const fallback = this.data === MISSING
|
|
74
|
+
const fallback = this.data === MISSING
|
|
75
|
+
? `must be ${expected} (was missing)`
|
|
76
|
+
: actual === ""
|
|
77
|
+
? `must be ${expected}`
|
|
78
|
+
: `must be ${expected} (was ${actual})`;
|
|
51
79
|
return format(this.#config?.problem, this.#context(expected, actual, fallback), fallback);
|
|
52
80
|
}
|
|
53
|
-
/** Full message including the path prefix. */
|
|
54
81
|
get message() {
|
|
55
82
|
const expected = this.expected;
|
|
56
83
|
const actual = this.actual;
|
|
57
84
|
const problem = this.problem;
|
|
58
|
-
const at = this.path.length === 0 ? "" : `${this.path
|
|
59
|
-
|
|
85
|
+
const at = this.path.length === 0 ? "" : `${formatPath(this.path)} `;
|
|
86
|
+
const messageActual = this.#config?.actual === undefined ? describeKind(this.data) : actual;
|
|
87
|
+
return format(this.#config?.message, this.#context(expected, messageActual, problem), `${at}${problem}`);
|
|
60
88
|
}
|
|
61
89
|
toString() {
|
|
62
90
|
return this.message;
|
|
@@ -64,7 +92,26 @@ export class OmpError {
|
|
|
64
92
|
}
|
|
65
93
|
/** Sentinel for a required key that was absent (distinguishes from `undefined`). */
|
|
66
94
|
export const MISSING = Symbol("omptype.missing");
|
|
67
|
-
function
|
|
95
|
+
function stringifyValue(data) {
|
|
96
|
+
const seen = new WeakSet();
|
|
97
|
+
return (JSON.stringify(data, (_key, value) => {
|
|
98
|
+
if (typeof value !== "object" || value === null)
|
|
99
|
+
return value;
|
|
100
|
+
if (seen.has(value))
|
|
101
|
+
return "(cycle)";
|
|
102
|
+
seen.add(value);
|
|
103
|
+
return value;
|
|
104
|
+
}) ?? "an object");
|
|
105
|
+
}
|
|
106
|
+
function describeValue(data, code) {
|
|
107
|
+
if (data === MISSING)
|
|
108
|
+
return "missing";
|
|
109
|
+
if (code === "undeclared")
|
|
110
|
+
return "";
|
|
111
|
+
if (code === "referenceSame")
|
|
112
|
+
return "";
|
|
113
|
+
if (code === "domain")
|
|
114
|
+
return describeKind(data);
|
|
68
115
|
if (data === null)
|
|
69
116
|
return "null";
|
|
70
117
|
if (Array.isArray(data))
|
|
@@ -81,47 +128,150 @@ function describeValue(data) {
|
|
|
81
128
|
case "undefined":
|
|
82
129
|
return "undefined";
|
|
83
130
|
case "object":
|
|
84
|
-
return
|
|
131
|
+
return stringifyValue(data);
|
|
85
132
|
case "function":
|
|
86
133
|
return "a function";
|
|
87
134
|
default:
|
|
88
135
|
return "a symbol";
|
|
89
136
|
}
|
|
90
137
|
}
|
|
138
|
+
function describeKind(data) {
|
|
139
|
+
if (data === MISSING)
|
|
140
|
+
return "missing";
|
|
141
|
+
if (typeof data === "number" && Number.isNaN(data))
|
|
142
|
+
return "NaN";
|
|
143
|
+
if (data === null)
|
|
144
|
+
return "null";
|
|
145
|
+
if (Array.isArray(data))
|
|
146
|
+
return "an object";
|
|
147
|
+
switch (typeof data) {
|
|
148
|
+
case "string":
|
|
149
|
+
return "a string";
|
|
150
|
+
case "number":
|
|
151
|
+
return "a number";
|
|
152
|
+
case "bigint":
|
|
153
|
+
return "a bigint";
|
|
154
|
+
case "boolean":
|
|
155
|
+
return "boolean";
|
|
156
|
+
case "undefined":
|
|
157
|
+
return "undefined";
|
|
158
|
+
case "object": {
|
|
159
|
+
const ctor = Object.getPrototypeOf(data)?.constructor;
|
|
160
|
+
return typeof ctor?.name === "string" && ctor.name !== "Object" ? ctor.name : "an object";
|
|
161
|
+
}
|
|
162
|
+
case "function":
|
|
163
|
+
return "a function";
|
|
164
|
+
default:
|
|
165
|
+
return "a symbol";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function describeExpectation(expected) {
|
|
169
|
+
const divisor = /divisible by (\d+(?:\.\d+)?)/.exec(expected);
|
|
170
|
+
if (divisor) {
|
|
171
|
+
const rule = Number(divisor[1]);
|
|
172
|
+
return { description: rule === 2 ? "even" : `divisible by ${rule}`, rule };
|
|
173
|
+
}
|
|
174
|
+
return { description: expected.replace(/^(?:an?|the) /, "") };
|
|
175
|
+
}
|
|
176
|
+
function formatPath(path) {
|
|
177
|
+
let out = "";
|
|
178
|
+
for (const key of path) {
|
|
179
|
+
if (typeof key === "number")
|
|
180
|
+
out += `[${key}]`;
|
|
181
|
+
else if (typeof key === "symbol")
|
|
182
|
+
out += `[${String(key)}]`;
|
|
183
|
+
else
|
|
184
|
+
out += out.length === 0 ? key : `.${key}`;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
91
188
|
function errorCode(expected, data) {
|
|
92
189
|
if (data === MISSING)
|
|
93
190
|
return "required";
|
|
94
|
-
if (expected
|
|
191
|
+
if (expected === "removed")
|
|
192
|
+
return "undeclared";
|
|
193
|
+
if (expected.includes("serialized to the same value"))
|
|
194
|
+
return "referenceSame";
|
|
195
|
+
if (expected.includes("divisible by") || expected.includes("integer"))
|
|
95
196
|
return "divisor";
|
|
96
|
-
if (expected
|
|
197
|
+
if (expected === "true" ||
|
|
198
|
+
expected === "false" ||
|
|
199
|
+
expected === "null" ||
|
|
200
|
+
expected === "undefined" ||
|
|
201
|
+
expected === "NaN" ||
|
|
202
|
+
expected === "Infinity" ||
|
|
203
|
+
expected === "-Infinity" ||
|
|
204
|
+
/^-?\d+(?:\.\d+)?n?$/.test(expected) ||
|
|
205
|
+
(expected.includes(" or ") &&
|
|
206
|
+
expected.split(" or ").every(part => /^".*"$|^-?\d+(?:\.\d+)?n?$|^(?:true|false|null|undefined)$/.test(part)))) {
|
|
207
|
+
return "unit";
|
|
208
|
+
}
|
|
209
|
+
if (expected.includes("at least") ||
|
|
210
|
+
expected.includes("more than") ||
|
|
211
|
+
expected.includes("timestamp after") ||
|
|
212
|
+
expected === "non-negative" ||
|
|
213
|
+
expected === "positive") {
|
|
97
214
|
return "min";
|
|
98
|
-
|
|
215
|
+
}
|
|
216
|
+
if (expected.includes("at most") ||
|
|
217
|
+
expected.includes("less than") ||
|
|
218
|
+
expected.includes("timestamp before") ||
|
|
219
|
+
expected === "non-positive" ||
|
|
220
|
+
expected === "negative") {
|
|
99
221
|
return "max";
|
|
100
|
-
|
|
222
|
+
}
|
|
223
|
+
if (expected.includes("matching") ||
|
|
224
|
+
expected.includes("format") ||
|
|
225
|
+
expected.includes("email") ||
|
|
226
|
+
expected.includes("parsable") ||
|
|
227
|
+
expected.includes("only digits")) {
|
|
101
228
|
return "pattern";
|
|
102
|
-
|
|
229
|
+
}
|
|
230
|
+
if (expected.includes("predicate") || expected.includes("satisfying") || expected.includes("according to"))
|
|
103
231
|
return "predicate";
|
|
104
232
|
if (expected.startsWith('"') || expected.startsWith("the date "))
|
|
105
233
|
return "unit";
|
|
106
|
-
|
|
234
|
+
if (expected === "an Error" || expected === "a Date" || expected.startsWith("an instance of "))
|
|
235
|
+
return "domain";
|
|
236
|
+
if ((expected.includes(" or ") && !expected.includes("IPv")) ||
|
|
237
|
+
expected.endsWith(" instance") ||
|
|
238
|
+
expected.startsWith("a number representing") ||
|
|
239
|
+
[
|
|
240
|
+
"a string",
|
|
241
|
+
"a number",
|
|
242
|
+
"a bigint",
|
|
243
|
+
"a symbol",
|
|
244
|
+
"boolean",
|
|
245
|
+
"an object",
|
|
246
|
+
"an array",
|
|
247
|
+
"a tuple",
|
|
248
|
+
"undefined",
|
|
249
|
+
"null",
|
|
250
|
+
"unknown",
|
|
251
|
+
"never",
|
|
252
|
+
].includes(expected))
|
|
253
|
+
return "domain";
|
|
254
|
+
return "predicate";
|
|
107
255
|
}
|
|
108
256
|
export class OmpErrors {
|
|
109
257
|
#path;
|
|
110
258
|
#expected;
|
|
111
259
|
#data;
|
|
112
260
|
#entry;
|
|
261
|
+
#entries;
|
|
113
262
|
#config;
|
|
114
|
-
|
|
115
|
-
length = 1;
|
|
263
|
+
#separator = "\n";
|
|
116
264
|
constructor(path, expected, data, config) {
|
|
117
265
|
this.#path = path;
|
|
118
266
|
this.#expected = expected;
|
|
119
267
|
this.#data = data;
|
|
120
268
|
this.#config = config;
|
|
121
269
|
}
|
|
122
|
-
|
|
270
|
+
get length() {
|
|
271
|
+
return this.#entries?.length ?? 1;
|
|
272
|
+
}
|
|
123
273
|
get 0() {
|
|
124
|
-
return this.#getEntry();
|
|
274
|
+
return this.#entries?.[0] ?? this.#getEntry();
|
|
125
275
|
}
|
|
126
276
|
static single(path, expected, data, config) {
|
|
127
277
|
return new OmpErrors(path, expected, data, config);
|
|
@@ -134,45 +284,90 @@ export class OmpErrors {
|
|
|
134
284
|
this.#entry = entry;
|
|
135
285
|
return entry;
|
|
136
286
|
}
|
|
137
|
-
/**
|
|
287
|
+
/** Append all failures from `other`, preserving traversal order. */
|
|
288
|
+
append(other) {
|
|
289
|
+
this.#entries ??= [this.#getEntry()];
|
|
290
|
+
const entries = this.#entries;
|
|
291
|
+
for (const entry of other)
|
|
292
|
+
entries.push(entry);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
/** Prefix every failure path with `key` when nesting sub-schemas. */
|
|
138
296
|
prefix(key) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
297
|
+
if (this.#entries) {
|
|
298
|
+
for (const entry of this.#entries)
|
|
299
|
+
entry.prefix(key);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const path = this.#path;
|
|
303
|
+
this.#path = path === undefined ? [key] : Array.isArray(path) ? [key, ...path] : [key, path];
|
|
304
|
+
this.#entry = undefined;
|
|
305
|
+
}
|
|
142
306
|
return this;
|
|
143
307
|
}
|
|
144
|
-
/** Apply schema-local message formatting without rebuilding
|
|
308
|
+
/** Apply schema-local message formatting without rebuilding failures. */
|
|
145
309
|
configure(config) {
|
|
146
|
-
this.#
|
|
147
|
-
|
|
310
|
+
if (this.#entries) {
|
|
311
|
+
for (const entry of this.#entries)
|
|
312
|
+
entry.configure(config);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.#config = { ...this.#config, ...config };
|
|
316
|
+
this.#entry = undefined;
|
|
317
|
+
}
|
|
148
318
|
return this;
|
|
149
319
|
}
|
|
150
|
-
/** Index the failure by its dotted property path (`""` for the root). */
|
|
151
320
|
get byPath() {
|
|
152
|
-
const
|
|
153
|
-
|
|
321
|
+
const result = {};
|
|
322
|
+
for (const entry of this)
|
|
323
|
+
result[entry.path.map(String).join(".")] = entry;
|
|
324
|
+
return result;
|
|
154
325
|
}
|
|
155
|
-
/** Transform the failure entry into a plain array. */
|
|
156
326
|
map(fn) {
|
|
157
|
-
|
|
327
|
+
const result = [];
|
|
328
|
+
let index = 0;
|
|
329
|
+
for (const entry of this)
|
|
330
|
+
result.push(fn(entry, index++, this));
|
|
331
|
+
return result;
|
|
158
332
|
}
|
|
159
|
-
/** Select the failure entry into a plain array. */
|
|
160
333
|
filter(fn) {
|
|
161
|
-
const
|
|
162
|
-
|
|
334
|
+
const result = [];
|
|
335
|
+
let index = 0;
|
|
336
|
+
for (const entry of this) {
|
|
337
|
+
if (fn(entry, index++, this))
|
|
338
|
+
result.push(entry);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
163
341
|
}
|
|
164
|
-
/** Iterate over the single failure entry. */
|
|
165
342
|
*[Symbol.iterator]() {
|
|
166
|
-
|
|
343
|
+
if (this.#entries) {
|
|
344
|
+
yield* this.#entries;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
yield this.#getEntry();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** @internal Render multiple branch failures as alternatives rather than independent failures. */
|
|
351
|
+
asAlternatives() {
|
|
352
|
+
this.#separator = " or ";
|
|
353
|
+
return this;
|
|
167
354
|
}
|
|
168
|
-
/** Human-readable failure text, materialized only when requested. */
|
|
169
355
|
get summary() {
|
|
170
|
-
|
|
356
|
+
const entries = [...this];
|
|
357
|
+
if (this.#separator === "\n" &&
|
|
358
|
+
entries.length > 1 &&
|
|
359
|
+
entries.every(entry => Object.is(entry.data, entries[0].data) &&
|
|
360
|
+
entry.path.length === entries[0].path.length &&
|
|
361
|
+
entry.path.every((key, index) => key === entries[0].path[index]))) {
|
|
362
|
+
const at = formatPath(entries[0].path);
|
|
363
|
+
const actual = typeof entries[0].data === "string" ? JSON.stringify(entries[0].data) : String(entries[0].data);
|
|
364
|
+
return `${at}${at === "" ? "" : " "}(${actual}) must be...\n${entries.map(entry => ` ◦ ${entry.expected}`).join("\n")}`;
|
|
365
|
+
}
|
|
366
|
+
return entries.map(error => error.message).join(this.#separator);
|
|
171
367
|
}
|
|
172
368
|
toString() {
|
|
173
369
|
return this.summary;
|
|
174
370
|
}
|
|
175
|
-
/** Throw a `TraversalError` carrying this result. */
|
|
176
371
|
throw() {
|
|
177
372
|
throw new TraversalError(this);
|
|
178
373
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema → omptype importer.
|
|
3
|
+
*
|
|
4
|
+
* Rebuilds a callable schema from JSON Schema documents — the inverse of
|
|
5
|
+
* `Type.toJsonSchema()` for the dialect omptype (and its Zod/TypeBox
|
|
6
|
+
* adapters) emits: draft-07 and draft-2020-12 structural keywords, string
|
|
7
|
+
* formats, `$defs`/`definitions` references (including recursion), enums,
|
|
8
|
+
* and composition via `anyOf`/`oneOf`/`allOf`.
|
|
9
|
+
*
|
|
10
|
+
* Unknown or non-structural keywords are ignored, matching lenient importer
|
|
11
|
+
* behavior: the result validates every constraint the schema can express in
|
|
12
|
+
* omptype IR and passes the rest through.
|
|
13
|
+
*/
|
|
14
|
+
import { OmpTypeError } from "./errors.js";
|
|
15
|
+
import { IR_BRAND } from "./ir.js";
|
|
16
|
+
import { keywordIR, patternIR } from "./keywords.js";
|
|
17
|
+
import { type } from "./type.js";
|
|
18
|
+
/** `format` values lowered to built-in keyword validators. */
|
|
19
|
+
const FORMAT_KEYWORDS = {
|
|
20
|
+
email: "string.email",
|
|
21
|
+
uuid: "string.uuid",
|
|
22
|
+
"date-time": "string.date.iso",
|
|
23
|
+
date: "string.date.iso",
|
|
24
|
+
ipv4: "string.ip.v4",
|
|
25
|
+
ipv6: "string.ip.v6",
|
|
26
|
+
regex: "string.regex",
|
|
27
|
+
};
|
|
28
|
+
class Importer {
|
|
29
|
+
#root;
|
|
30
|
+
#aliases = new Map();
|
|
31
|
+
constructor(root) {
|
|
32
|
+
this.#root = root;
|
|
33
|
+
}
|
|
34
|
+
resolveRef(ref) {
|
|
35
|
+
const cached = this.#aliases.get(ref);
|
|
36
|
+
if (cached !== undefined)
|
|
37
|
+
return cached;
|
|
38
|
+
let target;
|
|
39
|
+
if (ref === "#") {
|
|
40
|
+
target = this.#root;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const defsMatch = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
|
|
44
|
+
if (defsMatch === null)
|
|
45
|
+
throw new OmpTypeError(`unsupported $ref: ${ref}`);
|
|
46
|
+
const defs = this.#root[defsMatch[1]];
|
|
47
|
+
target = typeof defs === "object" && defs !== null ? defs[defsMatch[2]] : undefined;
|
|
48
|
+
if (target === undefined)
|
|
49
|
+
throw new OmpTypeError(`unresolved $ref: ${ref}`);
|
|
50
|
+
}
|
|
51
|
+
// Register the alias before lowering so recursive references resolve
|
|
52
|
+
// to the same node instead of recursing forever.
|
|
53
|
+
let lowered;
|
|
54
|
+
const alias = {
|
|
55
|
+
k: "alias",
|
|
56
|
+
name: ref,
|
|
57
|
+
resolve: () => {
|
|
58
|
+
lowered ??= this.lower(target);
|
|
59
|
+
return lowered;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
this.#aliases.set(ref, alias);
|
|
63
|
+
return alias;
|
|
64
|
+
}
|
|
65
|
+
lower(schema) {
|
|
66
|
+
if (schema === true)
|
|
67
|
+
return { k: "unknown" };
|
|
68
|
+
if (schema === false)
|
|
69
|
+
return { k: "never" };
|
|
70
|
+
if (typeof schema !== "object" || schema === null) {
|
|
71
|
+
throw new OmpTypeError("JSON Schema nodes must be booleans or objects");
|
|
72
|
+
}
|
|
73
|
+
const node = schema;
|
|
74
|
+
const desc = typeof node.description === "string" ? node.description : undefined;
|
|
75
|
+
const ir = this.#lowerNode(node);
|
|
76
|
+
return desc === undefined ? ir : { ...ir, desc };
|
|
77
|
+
}
|
|
78
|
+
#lowerNode(node) {
|
|
79
|
+
if (typeof node.$ref === "string")
|
|
80
|
+
return this.resolveRef(node.$ref);
|
|
81
|
+
if (Array.isArray(node.enum)) {
|
|
82
|
+
const members = node.enum.map((v) => ({ k: "lit", v }));
|
|
83
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
84
|
+
}
|
|
85
|
+
if ("const" in node)
|
|
86
|
+
return { k: "lit", v: node.const };
|
|
87
|
+
const branches = node.anyOf ?? node.oneOf;
|
|
88
|
+
if (Array.isArray(branches)) {
|
|
89
|
+
return { k: "union", members: branches.map(branch => this.lower(branch)) };
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(node.allOf)) {
|
|
92
|
+
const members = node.allOf.map(branch => this.lower(branch));
|
|
93
|
+
return members.length === 1 ? members[0] : { k: "intersection", members };
|
|
94
|
+
}
|
|
95
|
+
if (typeof node.not === "object" && node.not !== null && Object.keys(node.not).length === 0) {
|
|
96
|
+
return { k: "never" };
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(node.type)) {
|
|
99
|
+
const members = node.type.map(t => this.#lowerTyped(node, String(t)));
|
|
100
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
101
|
+
}
|
|
102
|
+
if (typeof node.type === "string")
|
|
103
|
+
return this.#lowerTyped(node, node.type);
|
|
104
|
+
// No explicit type: infer from structural keywords, else accept anything.
|
|
105
|
+
if (node.properties !== undefined || node.required !== undefined)
|
|
106
|
+
return this.#lowerObject(node);
|
|
107
|
+
if (node.items !== undefined || node.prefixItems !== undefined)
|
|
108
|
+
return this.#lowerArray(node);
|
|
109
|
+
return { k: "unknown" };
|
|
110
|
+
}
|
|
111
|
+
#lowerTyped(node, kind) {
|
|
112
|
+
switch (kind) {
|
|
113
|
+
case "null":
|
|
114
|
+
return { k: "null" };
|
|
115
|
+
case "boolean":
|
|
116
|
+
return { k: "boolean" };
|
|
117
|
+
case "string":
|
|
118
|
+
return this.#lowerString(node);
|
|
119
|
+
case "number":
|
|
120
|
+
case "integer": {
|
|
121
|
+
const ir = { k: "number" };
|
|
122
|
+
if (kind === "integer")
|
|
123
|
+
ir.int = true;
|
|
124
|
+
if (typeof node.minimum === "number")
|
|
125
|
+
ir.min = node.minimum;
|
|
126
|
+
if (typeof node.maximum === "number")
|
|
127
|
+
ir.max = node.maximum;
|
|
128
|
+
if (typeof node.exclusiveMinimum === "number") {
|
|
129
|
+
ir.min = node.exclusiveMinimum;
|
|
130
|
+
ir.xmin = true;
|
|
131
|
+
}
|
|
132
|
+
if (typeof node.exclusiveMaximum === "number") {
|
|
133
|
+
ir.max = node.exclusiveMaximum;
|
|
134
|
+
ir.xmax = true;
|
|
135
|
+
}
|
|
136
|
+
if (typeof node.multipleOf === "number")
|
|
137
|
+
ir.divisor = node.multipleOf;
|
|
138
|
+
return ir;
|
|
139
|
+
}
|
|
140
|
+
case "object":
|
|
141
|
+
return this.#lowerObject(node);
|
|
142
|
+
case "array":
|
|
143
|
+
return this.#lowerArray(node);
|
|
144
|
+
default:
|
|
145
|
+
throw new OmpTypeError(`unsupported JSON Schema type: ${kind}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
#lowerString(node) {
|
|
149
|
+
const base = { k: "string" };
|
|
150
|
+
if (typeof node.minLength === "number")
|
|
151
|
+
base.min = node.minLength;
|
|
152
|
+
if (typeof node.maxLength === "number")
|
|
153
|
+
base.max = node.maxLength;
|
|
154
|
+
if (node.format === "uri" || node.format === "url")
|
|
155
|
+
base.url = true;
|
|
156
|
+
const members = [];
|
|
157
|
+
if (typeof node.format === "string") {
|
|
158
|
+
const keyword = FORMAT_KEYWORDS[node.format];
|
|
159
|
+
if (keyword !== undefined) {
|
|
160
|
+
const formatIR = keywordIR(keyword);
|
|
161
|
+
if (formatIR !== undefined)
|
|
162
|
+
members.push(formatIR);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (typeof node.pattern === "string")
|
|
166
|
+
members.push(patternIR(new RegExp(node.pattern)));
|
|
167
|
+
if (members.length === 0)
|
|
168
|
+
return base;
|
|
169
|
+
if (base.min !== undefined || base.max !== undefined || base.url)
|
|
170
|
+
members.unshift(base);
|
|
171
|
+
return members.length === 1 ? members[0] : { k: "intersection", members };
|
|
172
|
+
}
|
|
173
|
+
#lowerObject(node) {
|
|
174
|
+
const required = new Set(Array.isArray(node.required) ? node.required.map(String) : []);
|
|
175
|
+
const props = [];
|
|
176
|
+
if (typeof node.properties === "object" && node.properties !== null) {
|
|
177
|
+
for (const [key, value] of Object.entries(node.properties)) {
|
|
178
|
+
const prop = { key, opt: !required.has(key), val: this.lower(value) };
|
|
179
|
+
if (typeof value === "object" && value !== null && "default" in value) {
|
|
180
|
+
prop.hasDefault = true;
|
|
181
|
+
prop.def = value.default;
|
|
182
|
+
prop.opt = true;
|
|
183
|
+
}
|
|
184
|
+
props.push(prop);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const extra = node.additionalProperties;
|
|
188
|
+
return {
|
|
189
|
+
k: "object",
|
|
190
|
+
props,
|
|
191
|
+
extras: extra === false ? "reject" : "keep",
|
|
192
|
+
...(typeof extra === "object" && extra !== null ? { index: this.lower(extra) } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
#lowerArray(node) {
|
|
196
|
+
if (Array.isArray(node.prefixItems)) {
|
|
197
|
+
const minItems = typeof node.minItems === "number" ? node.minItems : node.prefixItems.length;
|
|
198
|
+
const prefix = node.prefixItems.map((item, index) => ({
|
|
199
|
+
val: this.lower(item),
|
|
200
|
+
opt: index >= minItems,
|
|
201
|
+
}));
|
|
202
|
+
return {
|
|
203
|
+
k: "tuple",
|
|
204
|
+
prefix,
|
|
205
|
+
postfix: [],
|
|
206
|
+
...(node.items !== undefined && node.items !== false ? { variadic: this.lower(node.items) } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const ir = { k: "array", el: node.items === undefined ? { k: "unknown" } : this.lower(node.items) };
|
|
210
|
+
if (typeof node.minItems === "number")
|
|
211
|
+
ir.min = node.minItems;
|
|
212
|
+
if (typeof node.maxItems === "number")
|
|
213
|
+
ir.max = node.maxItems;
|
|
214
|
+
return ir;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Build a callable omptype schema from a JSON Schema document.
|
|
219
|
+
*
|
|
220
|
+
* # Errors
|
|
221
|
+
* Throws {@link OmpTypeError} on malformed nodes, unresolvable `$ref`s, or
|
|
222
|
+
* types omptype cannot represent.
|
|
223
|
+
*/
|
|
224
|
+
export function fromJsonSchema(schema) {
|
|
225
|
+
const importer = new Importer(typeof schema === "object" && schema !== null ? schema : {});
|
|
226
|
+
const embedded = {
|
|
227
|
+
[IR_BRAND]: true,
|
|
228
|
+
ir: importer.lower(schema),
|
|
229
|
+
hasSteps: false,
|
|
230
|
+
hasDefault: false,
|
|
231
|
+
run: value => value,
|
|
232
|
+
};
|
|
233
|
+
return type.raw(embedded);
|
|
234
|
+
}
|