@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/json-schema.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** Emit the requested JSON Schema dialect represented by an IR tree. */
|
|
2
2
|
export function irToJsonSchema(ir, options) {
|
|
3
|
-
|
|
3
|
+
const ctx = { options, defs: new Map(), refs: new Map() };
|
|
4
|
+
let schema = emit(ir, ctx);
|
|
5
|
+
if (ctx.defs.size > 0)
|
|
6
|
+
schema.$defs = Object.fromEntries(ctx.defs);
|
|
4
7
|
if (options?.target === "draft-07")
|
|
5
8
|
schema = toDraft7(schema);
|
|
6
|
-
const dialect = options?.dialect ?? dialectFor(options?.target);
|
|
9
|
+
const dialect = options?.dialect === null ? undefined : (options?.dialect ?? dialectFor(options?.target));
|
|
7
10
|
if (dialect !== undefined)
|
|
8
11
|
schema.$schema = dialect;
|
|
9
12
|
if (options?.description !== undefined)
|
|
@@ -15,10 +18,10 @@ function dialectFor(target) {
|
|
|
15
18
|
return "https://json-schema.org/draft/2020-12/schema";
|
|
16
19
|
if (target === "draft-07")
|
|
17
20
|
return "http://json-schema.org/draft-07/schema#";
|
|
18
|
-
return target
|
|
21
|
+
return target !== undefined && (target.startsWith("http://") || target.startsWith("https://")) ? target : undefined;
|
|
19
22
|
}
|
|
20
|
-
function fallback(schema,
|
|
21
|
-
const replacement = options?.fallback?.({ base: schema });
|
|
23
|
+
function fallback(schema, ctx) {
|
|
24
|
+
const replacement = ctx.options?.fallback?.({ base: schema });
|
|
22
25
|
if (replacement === true)
|
|
23
26
|
return {};
|
|
24
27
|
if (replacement === false)
|
|
@@ -29,7 +32,13 @@ function toDraft7(schema) {
|
|
|
29
32
|
const converted = {};
|
|
30
33
|
for (const key in schema) {
|
|
31
34
|
const value = schema[key];
|
|
32
|
-
if (key === "
|
|
35
|
+
if (key === "$ref" && typeof value === "string") {
|
|
36
|
+
converted.$ref = value.replace("#/$defs/", "#/definitions/");
|
|
37
|
+
}
|
|
38
|
+
else if (key === "$defs") {
|
|
39
|
+
converted.definitions = toDraft7(value);
|
|
40
|
+
}
|
|
41
|
+
else if (key === "prefixItems" && Array.isArray(value)) {
|
|
33
42
|
converted.items = value.map(item => typeof item === "object" && item !== null ? toDraft7(item) : item);
|
|
34
43
|
}
|
|
35
44
|
else if (key === "items" && "prefixItems" in schema) {
|
|
@@ -48,14 +57,14 @@ function toDraft7(schema) {
|
|
|
48
57
|
}
|
|
49
58
|
return converted;
|
|
50
59
|
}
|
|
51
|
-
function emit(ir,
|
|
60
|
+
function emit(ir, ctx) {
|
|
52
61
|
let schema;
|
|
53
62
|
switch (ir.k) {
|
|
54
63
|
case "unknown":
|
|
55
64
|
schema = {};
|
|
56
65
|
break;
|
|
57
66
|
case "undefined":
|
|
58
|
-
schema = fallback({},
|
|
67
|
+
schema = fallback({}, ctx);
|
|
59
68
|
break;
|
|
60
69
|
case "null":
|
|
61
70
|
schema = { type: "null" };
|
|
@@ -67,7 +76,7 @@ function emit(ir, options) {
|
|
|
67
76
|
schema = { type: "integer" };
|
|
68
77
|
break;
|
|
69
78
|
case "symbol":
|
|
70
|
-
schema = fallback({},
|
|
79
|
+
schema = fallback({}, ctx);
|
|
71
80
|
break;
|
|
72
81
|
case "never":
|
|
73
82
|
schema = { not: {} };
|
|
@@ -85,13 +94,13 @@ function emit(ir, options) {
|
|
|
85
94
|
schema = emitLiteral(ir.v);
|
|
86
95
|
break;
|
|
87
96
|
case "union":
|
|
88
|
-
schema = emitUnion(ir.members,
|
|
97
|
+
schema = emitUnion(ir.members, ctx);
|
|
89
98
|
break;
|
|
90
99
|
case "intersection":
|
|
91
|
-
schema = { allOf: ir.members.map(member => emit(member,
|
|
100
|
+
schema = { allOf: ir.members.map(member => emit(member, ctx)) };
|
|
92
101
|
break;
|
|
93
102
|
case "array":
|
|
94
|
-
schema = { type: "array", items: emit(ir.el,
|
|
103
|
+
schema = { type: "array", items: emit(ir.el, ctx) };
|
|
95
104
|
if (ir.min !== undefined)
|
|
96
105
|
schema.minItems = ir.min;
|
|
97
106
|
if (ir.max !== undefined)
|
|
@@ -99,7 +108,7 @@ function emit(ir, options) {
|
|
|
99
108
|
break;
|
|
100
109
|
case "tuple": {
|
|
101
110
|
const prefixItems = ir.prefix.map(item => {
|
|
102
|
-
const itemSchema = emit(item.val,
|
|
111
|
+
const itemSchema = emit(item.val, ctx);
|
|
103
112
|
if (item.hasDefault) {
|
|
104
113
|
itemSchema.default = item.defFactory && typeof item.def === "function" ? item.def() : item.def;
|
|
105
114
|
}
|
|
@@ -108,38 +117,64 @@ function emit(ir, options) {
|
|
|
108
117
|
const required = ir.prefix.reduce((count, item) => count + (item.opt || item.hasDefault ? 0 : 1), ir.postfix.length);
|
|
109
118
|
schema = { type: "array", prefixItems, minItems: required };
|
|
110
119
|
if (ir.variadic === undefined) {
|
|
111
|
-
schema.maxItems = ir.prefix.length + ir.postfix.length;
|
|
112
120
|
schema.items = false;
|
|
113
121
|
}
|
|
114
122
|
else {
|
|
115
|
-
schema.items = emit(ir.variadic,
|
|
123
|
+
schema.items = emit(ir.variadic, ctx);
|
|
116
124
|
}
|
|
117
125
|
break;
|
|
118
126
|
}
|
|
119
127
|
case "object":
|
|
120
|
-
schema = emitObject(ir.props, ir.index, ir.extras,
|
|
128
|
+
schema = emitObject(ir.props, ir.index, ir.extras, ctx);
|
|
121
129
|
break;
|
|
122
130
|
case "instance":
|
|
123
|
-
schema = ir.ctor === Date ? { type: "string", format: "date-time" } : fallback({ type: "object" },
|
|
131
|
+
schema = ir.ctor === Date ? { type: "string", format: "date-time" } : fallback({ type: "object" }, ctx);
|
|
124
132
|
break;
|
|
125
133
|
case "refine":
|
|
126
|
-
schema = emit(ir.base,
|
|
134
|
+
schema = emit(ir.base, ctx);
|
|
127
135
|
if (ir.json !== undefined)
|
|
128
136
|
Object.assign(schema, ir.json);
|
|
129
137
|
break;
|
|
130
138
|
case "morph":
|
|
131
|
-
schema = fallback(emit(ir.out ?? ir.input,
|
|
139
|
+
schema = ctx.options?.io === "input" ? emit(ir.input, ctx) : fallback(emit(ir.out ?? ir.input, ctx), ctx);
|
|
132
140
|
break;
|
|
133
|
-
case "alias":
|
|
134
|
-
|
|
141
|
+
case "alias": {
|
|
142
|
+
const known = ctx.refs.get(ir.resolve);
|
|
143
|
+
if (known !== undefined) {
|
|
144
|
+
schema = { $ref: `#/$defs/${known}` };
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
// Register before lowering so cyclic aliases resolve to the same $ref
|
|
148
|
+
// instead of recursing forever.
|
|
149
|
+
let name = ir.name.replace(/[^\w.-]/g, "_");
|
|
150
|
+
while ([...ctx.refs.values()].includes(name))
|
|
151
|
+
name = `${name}_`;
|
|
152
|
+
ctx.refs.set(ir.resolve, name);
|
|
153
|
+
ctx.defs.set(name, emit(ir.resolve(), ctx));
|
|
154
|
+
schema = { $ref: `#/$defs/${name}` };
|
|
135
155
|
break;
|
|
156
|
+
}
|
|
136
157
|
case "sub":
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
if (ctx.options?.io === "output" && ir.schema.opaqueOutput) {
|
|
159
|
+
schema = {};
|
|
160
|
+
}
|
|
161
|
+
else if (ctx.options?.io === "output" && ir.schema.stepOut !== undefined) {
|
|
162
|
+
schema = emit(ir.schema.stepOut, ctx);
|
|
163
|
+
}
|
|
164
|
+
else if (ctx.options?.io === "input") {
|
|
165
|
+
schema = emit(ir.schema.ir, ctx);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
schema = ir.schema.hasSteps ? fallback(emit(ir.schema.ir, ctx), ctx) : emit(ir.schema.ir, ctx);
|
|
169
|
+
}
|
|
170
|
+
if (ir.schema.ir.desc !== undefined)
|
|
171
|
+
schema.description = ir.schema.ir.desc;
|
|
172
|
+
else if (ir.schema.description !== undefined && ir.descAuto !== true) {
|
|
139
173
|
schema.description = ir.schema.description;
|
|
174
|
+
}
|
|
140
175
|
break;
|
|
141
176
|
}
|
|
142
|
-
if (ir.desc !== undefined)
|
|
177
|
+
if (ir.desc !== undefined && ir.descAuto !== true)
|
|
143
178
|
schema.description = ir.desc;
|
|
144
179
|
return schema;
|
|
145
180
|
}
|
|
@@ -183,12 +218,12 @@ function emitLiteral(value) {
|
|
|
183
218
|
return {};
|
|
184
219
|
}
|
|
185
220
|
}
|
|
186
|
-
function emitUnion(members,
|
|
221
|
+
function emitUnion(members, ctx) {
|
|
187
222
|
const defined = members.filter(member => member.k !== "undefined");
|
|
188
223
|
if (defined.length === 0)
|
|
189
224
|
return {};
|
|
190
225
|
if (defined.length === 1)
|
|
191
|
-
return emit(defined[0],
|
|
226
|
+
return emit(defined[0], ctx);
|
|
192
227
|
if (defined.every(member => member.k === "lit" && isJsonValue(member.v))) {
|
|
193
228
|
const values = defined.map(member => member.v);
|
|
194
229
|
const schema = { enum: values };
|
|
@@ -197,7 +232,7 @@ function emitUnion(members, options) {
|
|
|
197
232
|
schema.type = scalarType;
|
|
198
233
|
return schema;
|
|
199
234
|
}
|
|
200
|
-
return { anyOf: defined.map(member => emit(member,
|
|
235
|
+
return { anyOf: defined.map(member => emit(member, ctx)) };
|
|
201
236
|
}
|
|
202
237
|
function homogeneousScalarType(values) {
|
|
203
238
|
const first = jsonScalarType(values[0]);
|
|
@@ -221,26 +256,30 @@ function jsonScalarType(value) {
|
|
|
221
256
|
return undefined;
|
|
222
257
|
}
|
|
223
258
|
}
|
|
224
|
-
function emitObject(props, index, extras,
|
|
259
|
+
function emitObject(props, index, extras, ctx) {
|
|
225
260
|
const properties = {};
|
|
226
261
|
const required = [];
|
|
262
|
+
const filled = (prop) => !prop.opt && (ctx.options?.io === "output" || !prop.hasDefault);
|
|
227
263
|
// ArkType emits required properties first (each group in declaration
|
|
228
264
|
// order); downstream wire consumers rely on that stable ordering.
|
|
229
|
-
const ordered = [...props.filter(
|
|
265
|
+
const ordered = [...props.filter(filled), ...props.filter(prop => !filled(prop))];
|
|
230
266
|
for (const prop of ordered) {
|
|
231
|
-
|
|
267
|
+
if (typeof prop.key === "symbol")
|
|
268
|
+
throw new TypeError("Cannot convert a symbol to a string");
|
|
269
|
+
const key = String(prop.key);
|
|
270
|
+
const propertySchema = emit(prop.val, ctx);
|
|
232
271
|
if (prop.hasDefault) {
|
|
233
272
|
propertySchema.default = prop.defFactory ? prop.def() : prop.def;
|
|
234
273
|
}
|
|
235
|
-
properties[
|
|
236
|
-
if (
|
|
237
|
-
required.push(
|
|
274
|
+
properties[key] = propertySchema;
|
|
275
|
+
if (filled(prop))
|
|
276
|
+
required.push(key);
|
|
238
277
|
}
|
|
239
278
|
const schema = { type: "object", properties };
|
|
240
279
|
if (required.length > 0)
|
|
241
280
|
schema.required = required;
|
|
242
281
|
if (index !== undefined)
|
|
243
|
-
schema.additionalProperties = emit(index,
|
|
282
|
+
schema.additionalProperties = emit(index, ctx);
|
|
244
283
|
else if (extras === "reject")
|
|
245
284
|
schema.additionalProperties = false;
|
|
246
285
|
return schema;
|
package/dist/js/keywords.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const NUMERIC = /^
|
|
1
|
+
const NUMERIC = /^(?:(?!^-0\.?0*$)(?:-?(?:(?:0|[1-9]\d*)(?:\.\d+)?)|\.\d+?))$/;
|
|
2
2
|
const INTEGER = /^[+-]?(?:0|[1-9]\d*)$/;
|
|
3
3
|
const EMAIL = /^[\w%+.-]+@[\d.A-Za-z-]+\.[A-Za-z]{2,}$/;
|
|
4
4
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][\dA-Za-z-]*))*))?(?:\+([\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*))?$/;
|
|
@@ -89,8 +89,115 @@ function isLuhnValid(input) {
|
|
|
89
89
|
}
|
|
90
90
|
return value.length > 0 && sum % 10 === 0;
|
|
91
91
|
}
|
|
92
|
+
function finiteNumber(expected = "a number") {
|
|
93
|
+
return {
|
|
94
|
+
k: "refine",
|
|
95
|
+
base: { k: "unknown" },
|
|
96
|
+
pred: value => typeof value === "number" && !Number.isNaN(value),
|
|
97
|
+
expected,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function jsonObject() {
|
|
101
|
+
let value;
|
|
102
|
+
const resolveValue = () => value;
|
|
103
|
+
const object = {
|
|
104
|
+
k: "object",
|
|
105
|
+
props: [],
|
|
106
|
+
index: { k: "alias", name: "$jsonValue", resolve: resolveValue },
|
|
107
|
+
extras: "keep",
|
|
108
|
+
};
|
|
109
|
+
const array = {
|
|
110
|
+
k: "array",
|
|
111
|
+
el: { k: "alias", name: "$jsonValue", resolve: resolveValue },
|
|
112
|
+
desc: "an object",
|
|
113
|
+
};
|
|
114
|
+
value = {
|
|
115
|
+
k: "union",
|
|
116
|
+
members: [
|
|
117
|
+
object,
|
|
118
|
+
array,
|
|
119
|
+
{ k: "number" },
|
|
120
|
+
{ k: "string" },
|
|
121
|
+
{ k: "lit", v: false },
|
|
122
|
+
{ k: "null" },
|
|
123
|
+
{ k: "lit", v: true },
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
return object;
|
|
127
|
+
}
|
|
128
|
+
function instance(ctor, name) {
|
|
129
|
+
return { k: "instance", ctor, expected: `${/^[AEIOU]/.test(name) ? "an" : "a"} ${name} instance` };
|
|
130
|
+
}
|
|
92
131
|
const keywordFactories = {
|
|
93
132
|
string: () => ({ k: "string" }),
|
|
133
|
+
Key: () => ({ k: "union", members: [{ k: "string" }, { k: "symbol" }] }),
|
|
134
|
+
"unknown.any": () => ({ k: "unknown" }),
|
|
135
|
+
Array: () => ({ k: "array", el: { k: "unknown" } }),
|
|
136
|
+
Function: () => instance(Function, "Function"),
|
|
137
|
+
RegExp: () => instance(RegExp, "RegExp"),
|
|
138
|
+
File: () => instance(File, "File"),
|
|
139
|
+
Error: () => instance(Error, "Error"),
|
|
140
|
+
Set: () => instance(Set, "Set"),
|
|
141
|
+
Map: () => instance(Map, "Map"),
|
|
142
|
+
WeakSet: () => instance(WeakSet, "WeakSet"),
|
|
143
|
+
WeakMap: () => instance(WeakMap, "WeakMap"),
|
|
144
|
+
Promise: () => instance(Promise, "Promise"),
|
|
145
|
+
FormData: () => instance(FormData, "FormData"),
|
|
146
|
+
"FormData.parse": () => ({
|
|
147
|
+
k: "morph",
|
|
148
|
+
input: instance(FormData, "FormData"),
|
|
149
|
+
fn: value => {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const [key, entry] of value.entries()) {
|
|
152
|
+
const current = out[key];
|
|
153
|
+
out[key] = current === undefined ? entry : Array.isArray(current) ? [...current, entry] : [current, entry];
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
},
|
|
157
|
+
out: { k: "object", props: [], index: { k: "unknown" }, extras: "keep" },
|
|
158
|
+
}),
|
|
159
|
+
"object.json": jsonObject,
|
|
160
|
+
"object.json.stringify": () => ({
|
|
161
|
+
k: "morph",
|
|
162
|
+
input: jsonObject(),
|
|
163
|
+
fn: value => JSON.stringify(value),
|
|
164
|
+
out: { k: "string" },
|
|
165
|
+
}),
|
|
166
|
+
"number.epoch": () => ({
|
|
167
|
+
k: "refine",
|
|
168
|
+
base: {
|
|
169
|
+
k: "refine",
|
|
170
|
+
base: {
|
|
171
|
+
k: "refine",
|
|
172
|
+
base: finiteNumber("a number representing a Unix timestamp"),
|
|
173
|
+
pred: value => Number.isInteger(value),
|
|
174
|
+
expected: "an integer representing a Unix timestamp",
|
|
175
|
+
},
|
|
176
|
+
pred: value => value >= -8_640_000_000_000_000,
|
|
177
|
+
expected: "a Unix timestamp after -8640000000000000",
|
|
178
|
+
},
|
|
179
|
+
pred: value => value <= 8_640_000_000_000_000,
|
|
180
|
+
expected: "a Unix timestamp before 8640000000000000",
|
|
181
|
+
}),
|
|
182
|
+
"number.safe": () => ({
|
|
183
|
+
k: "refine",
|
|
184
|
+
base: {
|
|
185
|
+
k: "refine",
|
|
186
|
+
base: finiteNumber(),
|
|
187
|
+
pred: value => value >= Number.MIN_SAFE_INTEGER,
|
|
188
|
+
expected: `at least ${Number.MIN_SAFE_INTEGER}`,
|
|
189
|
+
},
|
|
190
|
+
pred: value => value <= Number.MAX_SAFE_INTEGER,
|
|
191
|
+
expected: `at most ${Number.MAX_SAFE_INTEGER}`,
|
|
192
|
+
}),
|
|
193
|
+
"number.NaN": () => ({
|
|
194
|
+
k: "refine",
|
|
195
|
+
base: { k: "unknown" },
|
|
196
|
+
pred: Number.isNaN,
|
|
197
|
+
expected: "NaN",
|
|
198
|
+
}),
|
|
199
|
+
"number.Infinity": () => ({ k: "lit", v: Number.POSITIVE_INFINITY }),
|
|
200
|
+
"number.NegativeInfinity": () => ({ k: "lit", v: Number.NEGATIVE_INFINITY }),
|
|
94
201
|
"string.alpha": () => pattern(/^[A-Za-z]*$/, "only letters"),
|
|
95
202
|
"string.alphanumeric": () => pattern(/^[\dA-Za-z]*$/, "only letters and digits 0-9"),
|
|
96
203
|
"string.hex": () => pattern(/^[\dA-Fa-f]+$/, "hex characters only"),
|