@oh-my-pi/omptype 17.2.7 → 17.2.9

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.
@@ -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
- let schema = emit(ir, options);
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?.startsWith("http://") || target?.startsWith("https://") ? target : undefined;
21
+ return target !== undefined && (target.startsWith("http://") || target.startsWith("https://")) ? target : undefined;
19
22
  }
20
- function fallback(schema, options) {
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 === "prefixItems" && Array.isArray(value)) {
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, options) {
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({}, options);
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({}, options);
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, options);
97
+ schema = emitUnion(ir.members, ctx);
89
98
  break;
90
99
  case "intersection":
91
- schema = { allOf: ir.members.map(member => emit(member, options)) };
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, options) };
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, options);
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, options);
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, options);
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" }, options);
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, options);
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, options), options);
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
- schema = emit(ir.resolve(), options);
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
- schema = ir.schema.hasSteps ? fallback(emit(ir.schema.ir, options), options) : emit(ir.schema.ir, options);
138
- if (ir.schema.description !== undefined)
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, options) {
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], options);
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, options)) };
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, options) {
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(p => !p.opt && !p.hasDefault), ...props.filter(p => p.opt || p.hasDefault)];
265
+ const ordered = [...props.filter(filled), ...props.filter(prop => !filled(prop))];
230
266
  for (const prop of ordered) {
231
- const propertySchema = emit(prop.val, options);
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[prop.key] = propertySchema;
236
- if (!prop.opt && !prop.hasDefault)
237
- required.push(prop.key);
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, options);
282
+ schema.additionalProperties = emit(index, ctx);
244
283
  else if (extras === "reject")
245
284
  schema.additionalProperties = false;
246
285
  return schema;
@@ -1,4 +1,4 @@
1
- const NUMERIC = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
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"),