@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.
@@ -0,0 +1,394 @@
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
+ function format(override, context, fallback) {
11
+ return typeof override === "function" ? override(context) : (override ?? fallback);
12
+ }
13
+ /** A single validation failure at one path. */
14
+ export class OmpError {
15
+ path;
16
+ data;
17
+ #rawExpected;
18
+ #config;
19
+ constructor(
20
+ /** Property path from the root to the failing value (empty at root). */
21
+ path, expected,
22
+ /** The value that failed validation. */
23
+ data, config) {
24
+ this.path = path;
25
+ this.data = data;
26
+ this.#rawExpected = expected;
27
+ this.#config = config;
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
+ }
39
+ /** Stable category for programmatic error handling. */
40
+ get code() {
41
+ return errorCode(this.#rawExpected, this.data);
42
+ }
43
+ #context(expected, actual, problem = "") {
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
+ };
56
+ }
57
+ /** Human-readable expectation, including a configured override. */
58
+ get expected() {
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);
63
+ }
64
+ /** Short description of the received value, e.g. `"a number"` or `"missing"`. */
65
+ get actual() {
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);
69
+ }
70
+ /** Path-less problem statement: `must be <expected> (was <actual>)`. */
71
+ get problem() {
72
+ const expected = this.expected;
73
+ const actual = this.actual;
74
+ const fallback = this.data === MISSING
75
+ ? `must be ${expected} (was missing)`
76
+ : actual === ""
77
+ ? `must be ${expected}`
78
+ : `must be ${expected} (was ${actual})`;
79
+ return format(this.#config?.problem, this.#context(expected, actual, fallback), fallback);
80
+ }
81
+ get message() {
82
+ const expected = this.expected;
83
+ const actual = this.actual;
84
+ const problem = this.problem;
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}`);
88
+ }
89
+ toString() {
90
+ return this.message;
91
+ }
92
+ }
93
+ /** Sentinel for a required key that was absent (distinguishes from `undefined`). */
94
+ export const MISSING = Symbol("omptype.missing");
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);
115
+ if (data === null)
116
+ return "null";
117
+ if (Array.isArray(data))
118
+ return "an array";
119
+ switch (typeof data) {
120
+ case "string":
121
+ return data.length <= 40 ? JSON.stringify(data) : `a string (length ${data.length})`;
122
+ case "number":
123
+ return String(data);
124
+ case "bigint":
125
+ return `${data}n`;
126
+ case "boolean":
127
+ return String(data);
128
+ case "undefined":
129
+ return "undefined";
130
+ case "object":
131
+ return stringifyValue(data);
132
+ case "function":
133
+ return "a function";
134
+ default:
135
+ return "a symbol";
136
+ }
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
+ }
188
+ function errorCode(expected, data) {
189
+ if (data === MISSING)
190
+ return "required";
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"))
196
+ return "divisor";
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") {
214
+ return "min";
215
+ }
216
+ if (expected.includes("at most") ||
217
+ expected.includes("less than") ||
218
+ expected.includes("timestamp before") ||
219
+ expected === "non-positive" ||
220
+ expected === "negative") {
221
+ return "max";
222
+ }
223
+ if (expected.includes("matching") ||
224
+ expected.includes("format") ||
225
+ expected.includes("email") ||
226
+ expected.includes("parsable") ||
227
+ expected.includes("only digits")) {
228
+ return "pattern";
229
+ }
230
+ if (expected.includes("predicate") || expected.includes("satisfying") || expected.includes("according to"))
231
+ return "predicate";
232
+ if (expected.startsWith('"') || expected.startsWith("the date "))
233
+ return "unit";
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";
255
+ }
256
+ export class OmpErrors {
257
+ #path;
258
+ #expected;
259
+ #data;
260
+ #entry;
261
+ #entries;
262
+ #config;
263
+ #separator = "\n";
264
+ constructor(path, expected, data, config) {
265
+ this.#path = path;
266
+ this.#expected = expected;
267
+ this.#data = data;
268
+ this.#config = config;
269
+ }
270
+ get length() {
271
+ return this.#entries?.length ?? 1;
272
+ }
273
+ get 0() {
274
+ return this.#entries?.[0] ?? this.#getEntry();
275
+ }
276
+ static single(path, expected, data, config) {
277
+ return new OmpErrors(path, expected, data, config);
278
+ }
279
+ #getEntry() {
280
+ if (this.#entry)
281
+ return this.#entry;
282
+ const path = this.#path === undefined ? [] : Array.isArray(this.#path) ? [...this.#path] : [this.#path];
283
+ const entry = new OmpError(path, this.#expected, this.#data, this.#config);
284
+ this.#entry = entry;
285
+ return entry;
286
+ }
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. */
296
+ prefix(key) {
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
+ }
306
+ return this;
307
+ }
308
+ /** Apply schema-local message formatting without rebuilding failures. */
309
+ configure(config) {
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
+ }
318
+ return this;
319
+ }
320
+ get byPath() {
321
+ const result = {};
322
+ for (const entry of this)
323
+ result[entry.path.map(String).join(".")] = entry;
324
+ return result;
325
+ }
326
+ map(fn) {
327
+ const result = [];
328
+ let index = 0;
329
+ for (const entry of this)
330
+ result.push(fn(entry, index++, this));
331
+ return result;
332
+ }
333
+ filter(fn) {
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;
341
+ }
342
+ *[Symbol.iterator]() {
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;
354
+ }
355
+ get summary() {
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);
367
+ }
368
+ toString() {
369
+ return this.summary;
370
+ }
371
+ throw() {
372
+ throw new TraversalError(this);
373
+ }
374
+ }
375
+ /** Error thrown by `Type.assert` on invalid input. */
376
+ export class TraversalError extends Error {
377
+ errors;
378
+ constructor(errors) {
379
+ super(errors.summary);
380
+ this.errors = errors;
381
+ this.name = "TraversalError";
382
+ }
383
+ }
384
+ /**
385
+ * Definition/usage error thrown while building a schema — malformed string
386
+ * DSL, unsupported composition, or an illegal builder call. Distinct from
387
+ * validation failures, which are returned as {@link OmpErrors}.
388
+ */
389
+ export class OmpTypeError extends Error {
390
+ constructor(message) {
391
+ super(message);
392
+ this.name = "OmpTypeError";
393
+ }
394
+ }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * omptype — ArkType-compatible schema validation with a lazy JIT runtime.
3
+ *
4
+ * ArkType-compatible `type()`/`Type`, keyword modules, recursive scopes,
5
+ * composition and morph APIs, structured errors, input/output inference, and
6
+ * JSON Schema emission.
7
+ */
8
+ export * from "./errors.js";
9
+ export * from "./from-json-schema.js";
10
+ export * from "./infer.js";
11
+ export * from "./ir.js";
12
+ export * from "./json-schema.js";
13
+ export * from "./type.js";
@@ -0,0 +1,2 @@
1
+ /** Type-level input and output inference for definitions accepted by omptype. */
2
+ export {};