@zudojs/openapi 0.0.1 → 1.1.0

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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -21
  3. package/dist/index.d.ts +26 -21
  4. package/dist/index.js +31 -13
  5. package/dist/openApiComponents/index.d.ts +2 -2
  6. package/dist/openApiComponents/index.js +1 -1
  7. package/dist/openApiConstants/index.d.ts +1 -1
  8. package/dist/openApiConstants/index.js +1 -1
  9. package/dist/openApiConstants/openApiConstants.core.d.ts +15 -3
  10. package/dist/openApiConstants/openApiConstants.core.js +22 -3
  11. package/dist/openApiDocument/index.d.ts +1 -1
  12. package/dist/openApiDocument/index.js +1 -1
  13. package/dist/openApiDocument/openApiDocument.builder.d.ts +39 -52
  14. package/dist/openApiDocument/openApiDocument.builder.js +73 -72
  15. package/dist/openApiErrors/index.d.ts +2 -2
  16. package/dist/openApiErrors/index.js +2 -1
  17. package/dist/openApiErrors/openApiError.base.d.ts +9 -0
  18. package/dist/openApiErrors/openApiError.base.js +7 -0
  19. package/dist/openApiErrors/openApiError.types.d.ts +34 -14
  20. package/dist/openApiErrors/openApiError.types.js +83 -35
  21. package/dist/openApiHttp/index.d.ts +2 -2
  22. package/dist/openApiHttp/index.js +2 -2
  23. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +127 -25
  24. package/dist/openApiHttp/openApiHttpAdapter.core.js +212 -16
  25. package/dist/openApiRegistry/openApiRegistry.core.d.ts +38 -5
  26. package/dist/openApiRegistry/openApiRegistry.core.js +167 -66
  27. package/dist/openApiRegistry/openApiRegistry.type.d.ts +18 -13
  28. package/dist/openApiRegistry/openApiRegistry.type.js +1 -2
  29. package/dist/openApiRouting/index.d.ts +2 -2
  30. package/dist/openApiRouting/index.js +1 -1
  31. package/dist/openApiRouting/routeConverter.core.d.ts +25 -7
  32. package/dist/openApiRouting/routeConverter.core.js +104 -58
  33. package/dist/openApiRouting/routeMetadata.type.d.ts +40 -17
  34. package/dist/openApiRouting/routeMetadata.type.js +9 -0
  35. package/dist/openApiRouting/routeScanner.core.d.ts +17 -47
  36. package/dist/openApiRouting/routeScanner.core.js +40 -55
  37. package/dist/openApiSchema/index.d.ts +4 -4
  38. package/dist/openApiSchema/index.js +2 -2
  39. package/dist/openApiSchema/references.core.d.ts +18 -1
  40. package/dist/openApiSchema/references.core.js +21 -1
  41. package/dist/openApiSchema/schemaConverter.core.d.ts +51 -3
  42. package/dist/openApiSchema/schemaConverter.core.js +475 -118
  43. package/dist/openApiSchema/schemaRegistry.core.d.ts +27 -10
  44. package/dist/openApiSchema/schemaRegistry.core.js +33 -14
  45. package/dist/openApiSerialization/openApiSerializer.core.d.ts +10 -4
  46. package/dist/openApiSerialization/openApiSerializer.core.js +135 -9
  47. package/dist/openApiTypes/index.d.ts +1 -1
  48. package/dist/openApiTypes/openApiDocument.type.d.ts +16 -0
  49. package/dist/openApiTypes/openApiSchema.type.d.ts +24 -1
  50. package/dist/openApiTypes/openApiSchema.type.js +3 -0
  51. package/dist/openApiUi/index.d.ts +8 -0
  52. package/dist/openApiUi/index.js +8 -0
  53. package/dist/openApiUi/openApiUi.brand.d.ts +26 -0
  54. package/dist/openApiUi/openApiUi.brand.js +44 -0
  55. package/dist/openApiUi/openApiUi.core.d.ts +47 -0
  56. package/dist/openApiUi/openApiUi.core.js +143 -0
  57. package/dist/openApiValidation/index.d.ts +2 -2
  58. package/dist/openApiValidation/index.js +1 -1
  59. package/dist/openApiValidation/openApiValidator.core.d.ts +33 -11
  60. package/dist/openApiValidation/openApiValidator.core.js +274 -66
  61. package/package.json +25 -18
  62. package/dist/.tsbuildinfo +0 -1
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js.map +0 -1
  65. package/dist/openApiComponents/index.d.ts.map +0 -1
  66. package/dist/openApiComponents/index.js.map +0 -1
  67. package/dist/openApiConstants/index.d.ts.map +0 -1
  68. package/dist/openApiConstants/index.js.map +0 -1
  69. package/dist/openApiConstants/openApiConstants.core.d.ts.map +0 -1
  70. package/dist/openApiConstants/openApiConstants.core.js.map +0 -1
  71. package/dist/openApiDocument/index.d.ts.map +0 -1
  72. package/dist/openApiDocument/index.js.map +0 -1
  73. package/dist/openApiDocument/openApiDocument.builder.d.ts.map +0 -1
  74. package/dist/openApiDocument/openApiDocument.builder.js.map +0 -1
  75. package/dist/openApiErrors/index.d.ts.map +0 -1
  76. package/dist/openApiErrors/index.js.map +0 -1
  77. package/dist/openApiErrors/openApiError.base.d.ts.map +0 -1
  78. package/dist/openApiErrors/openApiError.base.js.map +0 -1
  79. package/dist/openApiErrors/openApiError.core.d.ts.map +0 -1
  80. package/dist/openApiErrors/openApiError.core.js.map +0 -1
  81. package/dist/openApiErrors/openApiError.types.d.ts.map +0 -1
  82. package/dist/openApiErrors/openApiError.types.js.map +0 -1
  83. package/dist/openApiHttp/index.d.ts.map +0 -1
  84. package/dist/openApiHttp/index.js.map +0 -1
  85. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts.map +0 -1
  86. package/dist/openApiHttp/openApiHttpAdapter.core.js.map +0 -1
  87. package/dist/openApiRegistry/index.d.ts.map +0 -1
  88. package/dist/openApiRegistry/index.js.map +0 -1
  89. package/dist/openApiRegistry/openApiRegistry.core.d.ts.map +0 -1
  90. package/dist/openApiRegistry/openApiRegistry.core.js.map +0 -1
  91. package/dist/openApiRegistry/openApiRegistry.type.d.ts.map +0 -1
  92. package/dist/openApiRegistry/openApiRegistry.type.js.map +0 -1
  93. package/dist/openApiRouting/index.d.ts.map +0 -1
  94. package/dist/openApiRouting/index.js.map +0 -1
  95. package/dist/openApiRouting/routeConverter.core.d.ts.map +0 -1
  96. package/dist/openApiRouting/routeConverter.core.js.map +0 -1
  97. package/dist/openApiRouting/routeMetadata.type.d.ts.map +0 -1
  98. package/dist/openApiRouting/routeMetadata.type.js.map +0 -1
  99. package/dist/openApiRouting/routeScanner.core.d.ts.map +0 -1
  100. package/dist/openApiRouting/routeScanner.core.js.map +0 -1
  101. package/dist/openApiSchema/index.d.ts.map +0 -1
  102. package/dist/openApiSchema/index.js.map +0 -1
  103. package/dist/openApiSchema/references.core.d.ts.map +0 -1
  104. package/dist/openApiSchema/references.core.js.map +0 -1
  105. package/dist/openApiSchema/schemaConverter.core.d.ts.map +0 -1
  106. package/dist/openApiSchema/schemaConverter.core.js.map +0 -1
  107. package/dist/openApiSchema/schemaRegistry.core.d.ts.map +0 -1
  108. package/dist/openApiSchema/schemaRegistry.core.js.map +0 -1
  109. package/dist/openApiSerialization/index.d.ts.map +0 -1
  110. package/dist/openApiSerialization/index.js.map +0 -1
  111. package/dist/openApiSerialization/openApiSerializer.core.d.ts.map +0 -1
  112. package/dist/openApiSerialization/openApiSerializer.core.js.map +0 -1
  113. package/dist/openApiTypes/index.d.ts.map +0 -1
  114. package/dist/openApiTypes/index.js.map +0 -1
  115. package/dist/openApiTypes/openApiComponent.type.d.ts.map +0 -1
  116. package/dist/openApiTypes/openApiComponent.type.js.map +0 -1
  117. package/dist/openApiTypes/openApiDocument.type.d.ts.map +0 -1
  118. package/dist/openApiTypes/openApiDocument.type.js.map +0 -1
  119. package/dist/openApiTypes/openApiPath.type.d.ts.map +0 -1
  120. package/dist/openApiTypes/openApiPath.type.js.map +0 -1
  121. package/dist/openApiTypes/openApiSchema.type.d.ts.map +0 -1
  122. package/dist/openApiTypes/openApiSchema.type.js.map +0 -1
  123. package/dist/openApiTypes/openApiSecurity.type.d.ts.map +0 -1
  124. package/dist/openApiTypes/openApiSecurity.type.js.map +0 -1
  125. package/dist/openApiTypes/openApiTypes.core.d.ts.map +0 -1
  126. package/dist/openApiTypes/openApiTypes.core.js.map +0 -1
  127. package/dist/openApiValidation/index.d.ts.map +0 -1
  128. package/dist/openApiValidation/index.js.map +0 -1
  129. package/dist/openApiValidation/openApiValidator.core.d.ts.map +0 -1
  130. package/dist/openApiValidation/openApiValidator.core.js.map +0 -1
@@ -1,141 +1,498 @@
1
- function isOptional(value) {
2
- if (value && typeof value === "object" && "_type" in value) {
3
- return value._type === "optional";
4
- }
5
- return false;
1
+ import { DEFAULT_OPENAPI_VERSION } from "../openApiConstants/openApiConstants.core.js";
2
+ /** Maps `@zudojs/schema` string formats onto OpenAPI `format` values. */
3
+ const STRING_FORMATS = {
4
+ email: "email",
5
+ url: "uri",
6
+ uuid: "uuid",
7
+ "uuid-v4": "uuid",
8
+ datetime: "date-time",
9
+ date: "date",
10
+ time: "time",
11
+ ipv4: "ipv4",
12
+ ipv6: "ipv6",
13
+ };
14
+ /**
15
+ * Assigns a property that may be named `__proto__`.
16
+ *
17
+ * `properties["__proto__"] = schema` on a plain object literal sets the
18
+ * object's prototype instead of adding a member: the property vanishes from
19
+ * the generated document with no error anywhere. A schema field genuinely
20
+ * called `__proto__` is unusual; one supplied by an attacker to make a
21
+ * constraint disappear from the published contract is exactly the point.
22
+ */
23
+ function defineProperty(target, key, value) {
24
+ Object.defineProperty(target, key, {
25
+ value,
26
+ enumerable: true,
27
+ writable: true,
28
+ configurable: true,
29
+ });
30
+ }
31
+ function isSchemaLike(value) {
32
+ return (typeof value === "object" &&
33
+ value !== null &&
34
+ typeof value._type === "string");
35
+ }
36
+ function isOptionalSchema(value) {
37
+ return isSchemaLike(value) && value._type === "optional";
6
38
  }
7
39
  function extractMeta(schema) {
8
- const m = schema._metadata;
9
- if (!m)
40
+ const meta = schema._metadata;
41
+ if (!meta)
10
42
  return {};
11
43
  return {
12
- ...(m.description ? { description: m.description } : {}),
13
- ...(m.example !== undefined ? { example: m.example } : {}),
14
- ...(m.deprecated ? { deprecated: true } : {}),
15
- ...(m.title ? { title: m.title } : {}),
44
+ ...(meta.description ? { description: meta.description } : {}),
45
+ ...(meta.example !== undefined ? { example: meta.example } : {}),
46
+ ...(meta.deprecated ? { deprecated: true } : {}),
47
+ ...(meta.title ? { title: meta.title } : {}),
16
48
  };
17
49
  }
18
- export function convertSchema(input, _visited = new Set()) {
19
- if (input && typeof input === "object" && "_type" in input) {
20
- const schema = input;
21
- const meta = extractMeta(schema);
22
- switch (schema._type) {
23
- case "string":
24
- case "number":
25
- case "boolean":
26
- return { schema: { type: schema._type, ...meta }, warnings: [] };
27
- case "object": {
28
- const shape = schema.shape ?? {};
29
- const requiredKeys = schema.requiredKeys ?? [];
30
- const properties = {};
31
- const required = [];
32
- const warnings = [];
33
- for (const [key, value] of Object.entries(shape)) {
34
- const converted = convertSchema(value, _visited);
35
- properties[key] = converted.schema;
36
- warnings.push(...converted.warnings);
37
- if (!isOptional(value) && requiredKeys.includes(key))
38
- required.push(key);
39
- }
40
- return {
41
- schema: {
42
- type: "object",
43
- ...(Object.keys(properties).length > 0 ? { properties } : {}),
44
- ...(required.length > 0 ? { required } : {}),
45
- ...meta,
46
- },
47
- warnings,
48
- };
50
+ function config(schema) {
51
+ const raw = schema["_config"];
52
+ return typeof raw === "object" && raw !== null
53
+ ? raw
54
+ : {};
55
+ }
56
+ function num(value) {
57
+ return typeof value === "number" && Number.isFinite(value)
58
+ ? value
59
+ : undefined;
60
+ }
61
+ /** True when the document being produced follows OpenAPI 3.1 or later. */
62
+ export function isVersion31(version) {
63
+ return !version.startsWith("3.0");
64
+ }
65
+ /**
66
+ * Applies nullability in the way the target version expresses it.
67
+ *
68
+ * 3.1 dropped `nullable` in favour of a type union, so emitting `nullable`
69
+ * into a 3.1 document produces a schema validators quietly ignore.
70
+ */
71
+ function applyNullable(schema, version) {
72
+ if (!isVersion31(version)) {
73
+ return { ...schema, nullable: true };
74
+ }
75
+ if (schema.type === undefined) {
76
+ // No type to widen — express it as a union with the null type.
77
+ return { ...schema, anyOf: [...(schema.anyOf ?? []), { type: "null" }] };
78
+ }
79
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
80
+ if (types.includes("null"))
81
+ return schema;
82
+ return { ...schema, type: [...types, "null"] };
83
+ }
84
+ function convertString(schema, state) {
85
+ const c = config(schema);
86
+ const format = typeof c["format"] === "string" ? c["format"] : undefined;
87
+ const pattern = c["pattern"];
88
+ const exact = num(c["length"]);
89
+ if (format !== undefined && STRING_FORMATS[format] === undefined) {
90
+ state.warnings.push(`String format "${format}" has no OpenAPI equivalent; the constraint ` +
91
+ `was dropped from the schema.`);
92
+ }
93
+ if (pattern instanceof RegExp) {
94
+ // `pattern` in JSON Schema carries no flags. An `i` regex silently
95
+ // becomes case-sensitive in the document, so the published contract is
96
+ // stricter than the code that validates against it — the kind of drift
97
+ // that only shows up as a rejected request in production.
98
+ const meaningful = pattern.flags.replace(/[gy]/g, "");
99
+ if (meaningful.length > 0) {
100
+ state.warnings.push(`Pattern /${pattern.source}/${pattern.flags} has flags OpenAPI ` +
101
+ `cannot express; the emitted pattern is case- and mode-sensitive.`);
102
+ }
103
+ }
104
+ else if (pattern !== undefined && typeof pattern !== "string") {
105
+ state.warnings.push("A `pattern` constraint was not a RegExp and could not be emitted.");
106
+ }
107
+ return {
108
+ type: "string",
109
+ ...(format && STRING_FORMATS[format]
110
+ ? { format: STRING_FORMATS[format] }
111
+ : {}),
112
+ ...(exact !== undefined
113
+ ? { minLength: exact, maxLength: exact }
114
+ : {
115
+ ...(num(c["min"]) !== undefined ? { minLength: num(c["min"]) } : {}),
116
+ ...(num(c["max"]) !== undefined ? { maxLength: num(c["max"]) } : {}),
117
+ }),
118
+ ...(pattern instanceof RegExp
119
+ ? { pattern: pattern.source }
120
+ : typeof pattern === "string"
121
+ ? { pattern }
122
+ : {}),
123
+ };
124
+ }
125
+ /**
126
+ * Renders an exclusive bound the way the target version spells it.
127
+ *
128
+ * 3.1 (JSON Schema 2020-12) gives `exclusiveMinimum` the numeric bound. 3.0
129
+ * defines it as a *boolean* modifier on `minimum`, so emitting the number
130
+ * into a 3.0 document produces a keyword of the wrong type: a strict
131
+ * validator rejects the document and a lenient one ignores the bound — either
132
+ * way the constraint is gone.
133
+ */
134
+ function exclusiveBound(kind, value, version) {
135
+ if (isVersion31(version)) {
136
+ return kind === "minimum"
137
+ ? { exclusiveMinimum: value }
138
+ : { exclusiveMaximum: value };
139
+ }
140
+ return kind === "minimum"
141
+ ? { minimum: value, exclusiveMinimum: true }
142
+ : { maximum: value, exclusiveMaximum: true };
143
+ }
144
+ function convertNumber(schema, state) {
145
+ const c = config(schema);
146
+ const gt = num(c["gt"]);
147
+ const lt = num(c["lt"]);
148
+ const version = state.version;
149
+ const exclusiveMin = gt !== undefined
150
+ ? gt
151
+ : c["positive"] === true && num(c["min"]) === undefined
152
+ ? 0
153
+ : undefined;
154
+ const exclusiveMax = lt !== undefined
155
+ ? lt
156
+ : c["negative"] === true && num(c["max"]) === undefined
157
+ ? 0
158
+ : undefined;
159
+ return {
160
+ type: c["int"] === true ? "integer" : "number",
161
+ ...(num(c["min"]) !== undefined ? { minimum: num(c["min"]) } : {}),
162
+ ...(num(c["max"]) !== undefined ? { maximum: num(c["max"]) } : {}),
163
+ ...(num(c["multipleOf"]) !== undefined
164
+ ? { multipleOf: num(c["multipleOf"]) }
165
+ : {}),
166
+ ...(exclusiveMin !== undefined
167
+ ? exclusiveBound("minimum", exclusiveMin, version)
168
+ : {}),
169
+ ...(exclusiveMax !== undefined
170
+ ? exclusiveBound("maximum", exclusiveMax, version)
171
+ : {}),
172
+ };
173
+ }
174
+ /** Infers the JSON Schema type of a literal or enum member. */
175
+ function typeOfValue(value) {
176
+ if (typeof value === "string")
177
+ return "string";
178
+ if (typeof value === "number")
179
+ return Number.isInteger(value) ? "integer" : "number";
180
+ if (typeof value === "boolean")
181
+ return "boolean";
182
+ if (value === null)
183
+ return "null";
184
+ return undefined;
185
+ }
186
+ function enumSchema(values, version) {
187
+ const types = new Set(values.map(typeOfValue));
188
+ const type = types.size === 1 ? [...types][0] : undefined;
189
+ // `"null"` is not a type in 3.0; leaving the enum untyped is correct there.
190
+ const usable = type === "null" && version !== undefined && !isVersion31(version)
191
+ ? undefined
192
+ : type;
193
+ return {
194
+ ...(usable ? { type: usable } : {}),
195
+ enum: [...values],
196
+ };
197
+ }
198
+ function convertNode(input, state) {
199
+ if (state.depth > state.maxDepth) {
200
+ state.warnings.push(`Schema nesting exceeded the maximum depth of ${state.maxDepth}.`);
201
+ return {};
202
+ }
203
+ if (!isSchemaLike(input)) {
204
+ return convertLiteralValue(input, state);
205
+ }
206
+ // Cycle guard. `lazy` schemas exist precisely to describe self-referential
207
+ // shapes, so without this a comment-with-replies schema recurses until the
208
+ // stack gives out.
209
+ if (state.visited.has(input)) {
210
+ state.warnings.push(`Recursive schema detected at "${input._type}"; emitted an empty schema. ` +
211
+ `Register the schema as a named component and reference it with $ref.`);
212
+ return {};
213
+ }
214
+ state.visited.add(input);
215
+ state.depth++;
216
+ try {
217
+ return { ...convertSchemaNode(input, state), ...extractMeta(input) };
218
+ }
219
+ finally {
220
+ state.depth--;
221
+ state.visited.delete(input);
222
+ }
223
+ }
224
+ function convertSchemaNode(schema, state) {
225
+ const c = config(schema);
226
+ switch (schema._type) {
227
+ case "string":
228
+ case "coerce.string":
229
+ return convertString(schema, state);
230
+ case "number":
231
+ case "coerce.number":
232
+ return convertNumber(schema, state);
233
+ case "boolean":
234
+ case "coerce.boolean":
235
+ return { type: "boolean" };
236
+ case "coerce.bigint":
237
+ return { type: "string", format: "int64" };
238
+ case "null":
239
+ // 3.0 has no `null` type; `nullable` on an untyped schema is the
240
+ // closest it can express.
241
+ return isVersion31(state.version)
242
+ ? { type: "null" }
243
+ : { nullable: true };
244
+ case "any":
245
+ case "unknown":
246
+ return {};
247
+ case "never":
248
+ return { not: {} };
249
+ case "undefined":
250
+ state.warnings.push("An `undefined` schema has no OpenAPI equivalent; emitted an empty schema.");
251
+ return {};
252
+ case "object": {
253
+ const shape = c["shape"] ?? {};
254
+ const requiredKeys = c["requiredKeys"];
255
+ const properties = {};
256
+ const required = [];
257
+ for (const [key, value] of Object.entries(shape)) {
258
+ defineProperty(properties, key, convertNode(value, state));
259
+ // A field is required unless it is wrapped in `optional`. An explicit
260
+ // `requiredKeys` set (from `.required()`) forces it back on.
261
+ const forced = requiredKeys instanceof Set && requiredKeys.has(key);
262
+ if (forced || !isOptionalSchema(value))
263
+ required.push(key);
49
264
  }
50
- case "array": {
51
- const items = schema.items;
52
- const warnings = [];
53
- let itemsSchema;
54
- if (items) {
55
- const converted = convertSchema(items, _visited);
56
- itemsSchema = converted.schema;
57
- warnings.push(...converted.warnings);
58
- }
265
+ const unknownKeys = c["unknownKeys"];
266
+ return {
267
+ type: "object",
268
+ ...(Object.keys(properties).length > 0 ? { properties } : {}),
269
+ ...(required.length > 0 ? { required } : {}),
270
+ ...(unknownKeys === "strip" || unknownKeys === "strict"
271
+ ? { additionalProperties: false }
272
+ : {}),
273
+ };
274
+ }
275
+ case "record": {
276
+ const valueSchema = c["_valueSchema"] ?? schema["_valueSchema"];
277
+ return {
278
+ type: "object",
279
+ additionalProperties: valueSchema === undefined ? true : convertNode(valueSchema, state),
280
+ };
281
+ }
282
+ case "array": {
283
+ const items = c["itemSchema"];
284
+ const exact = num(c["length"]);
285
+ return {
286
+ type: "array",
287
+ ...(items !== undefined ? { items: convertNode(items, state) } : {}),
288
+ ...(exact !== undefined
289
+ ? { minItems: exact, maxItems: exact }
290
+ : {
291
+ ...(num(c["min"]) !== undefined
292
+ ? { minItems: num(c["min"]) }
293
+ : {}),
294
+ ...(num(c["max"]) !== undefined
295
+ ? { maxItems: num(c["max"]) }
296
+ : {}),
297
+ }),
298
+ };
299
+ }
300
+ case "set": {
301
+ const valueSchema = schema["_valueSchema"];
302
+ return {
303
+ type: "array",
304
+ uniqueItems: true,
305
+ ...(valueSchema !== undefined
306
+ ? { items: convertNode(valueSchema, state) }
307
+ : {}),
308
+ };
309
+ }
310
+ case "map": {
311
+ const valueSchema = schema["_valueSchema"];
312
+ state.warnings.push("A `map` schema is represented as an object with additionalProperties; " +
313
+ "non-string keys cannot be expressed in OpenAPI.");
314
+ return {
315
+ type: "object",
316
+ additionalProperties: valueSchema === undefined ? true : convertNode(valueSchema, state),
317
+ };
318
+ }
319
+ case "tuple": {
320
+ const schemas = schema["_schemas"];
321
+ if (!Array.isArray(schemas))
322
+ return { type: "array" };
323
+ const items = schemas.map((entry) => convertNode(entry, state));
324
+ if (!isVersion31(state.version)) {
325
+ // 3.0 has no positional items. `anyOf` over the member schemas keeps
326
+ // the length constraint honest without claiming a per-position type
327
+ // the version cannot express; emitting `prefixItems` instead produces
328
+ // a keyword every 3.0 validator ignores.
329
+ state.warnings.push("A `tuple` schema cannot express positional item types in " +
330
+ "OpenAPI 3.0; emitted a length-constrained array instead.");
59
331
  return {
60
- schema: {
61
- type: "array",
62
- ...(itemsSchema ? { items: itemsSchema } : {}),
63
- ...meta,
64
- },
65
- warnings,
332
+ type: "array",
333
+ ...(items.length > 0 ? { items: { anyOf: items } } : {}),
334
+ minItems: items.length,
335
+ maxItems: items.length,
66
336
  };
67
337
  }
68
- case "enum":
69
- case "literal":
70
- return {
71
- schema: {
72
- type: "string",
73
- enum: schema.values ?? [],
74
- ...meta,
75
- },
76
- warnings: [],
77
- };
78
- case "union": {
79
- const options = schema.options ?? [];
80
- const warnings = [];
81
- const oneOf = options.map((option) => {
82
- const converted = convertSchema(option, _visited);
83
- warnings.push(...converted.warnings);
84
- return converted.schema;
85
- });
86
- return { schema: { oneOf, ...meta }, warnings };
338
+ return {
339
+ type: "array",
340
+ prefixItems: items,
341
+ minItems: items.length,
342
+ maxItems: items.length,
343
+ };
344
+ }
345
+ case "enum": {
346
+ const values = schema["_values"];
347
+ if (!Array.isArray(values) || values.length === 0) {
348
+ state.warnings.push("An `enum` schema had no values; `enum` must be non-empty in OpenAPI.");
349
+ return {};
87
350
  }
88
- case "optional": {
89
- const inner = schema.inner;
90
- if (!inner)
91
- return {
92
- schema: {},
93
- warnings: ["Optional schema has no inner schema."],
94
- };
95
- return convertSchema(inner, _visited);
351
+ return enumSchema(values, state.version);
352
+ }
353
+ case "literal": {
354
+ const value = schema["_expected"];
355
+ if (value === undefined) {
356
+ state.warnings.push("A `literal` schema had no value.");
357
+ return {};
96
358
  }
97
- case "nullable": {
98
- const inner = schema.inner;
99
- if (!inner)
100
- return {
101
- schema: { nullable: true },
102
- warnings: ["Nullable schema has no inner schema."],
103
- };
104
- const converted = convertSchema(inner, _visited);
105
- return {
106
- schema: { ...converted.schema, nullable: true },
107
- warnings: converted.warnings,
108
- };
359
+ // `const` arrived with JSON Schema 2020-12; a 3.0 document expresses a
360
+ // single permitted value as a one-member enum.
361
+ return isVersion31(state.version)
362
+ ? { ...enumSchema([value], state.version), const: value }
363
+ : enumSchema([value], state.version);
364
+ }
365
+ case "union": {
366
+ const schemas = schema["_schemas"];
367
+ if (!Array.isArray(schemas) || schemas.length === 0) {
368
+ state.warnings.push("A `union` schema had no members.");
369
+ return {};
109
370
  }
110
- default:
111
- return {
112
- schema: {},
113
- warnings: [
114
- `Unsupported schema type: ${schema._type ?? "unknown"}`,
115
- ],
116
- };
371
+ return { anyOf: schemas.map((entry) => convertNode(entry, state)) };
117
372
  }
118
- }
119
- if (Array.isArray(input)) {
120
- const items = input[0];
121
- if (items) {
122
- const converted = convertSchema(items, _visited);
373
+ case "discriminatedUnion": {
374
+ const map = schema["_schemaMap"];
375
+ const discriminator = schema["_discriminator"];
376
+ const members = map instanceof Map ? [...map.values()] : [];
377
+ if (members.length === 0) {
378
+ state.warnings.push("A `discriminatedUnion` schema had no members.");
379
+ return {};
380
+ }
123
381
  return {
124
- schema: { type: "array", items: converted.schema },
125
- warnings: converted.warnings,
382
+ oneOf: members.map((entry) => convertNode(entry, state)),
383
+ ...(typeof discriminator === "string"
384
+ ? { discriminator: { propertyName: discriminator } }
385
+ : {}),
126
386
  };
127
387
  }
128
- return { schema: { type: "array" }, warnings: [] };
388
+ case "intersection": {
389
+ const left = schema["_left"];
390
+ const right = schema["_right"];
391
+ const parts = [left, right].filter((part) => part !== undefined);
392
+ if (parts.length === 0) {
393
+ state.warnings.push("An `intersection` schema had no members.");
394
+ return {};
395
+ }
396
+ return { allOf: parts.map((part) => convertNode(part, state)) };
397
+ }
398
+ case "optional":
399
+ // Optionality is expressed by the parent's `required` list, not by the
400
+ // property schema, so the wrapper contributes nothing of its own.
401
+ return convertInner(schema["_inner"], state, "optional");
402
+ case "nullable":
403
+ return applyNullable(convertInner(schema["_inner"], state, "nullable"), state.version);
404
+ case "default": {
405
+ const inner = convertInner(schema["_inner"], state, "default");
406
+ const defaultValue = schema["_defaultValue"];
407
+ return defaultValue === undefined
408
+ ? inner
409
+ : { ...inner, default: defaultValue };
410
+ }
411
+ case "refine":
412
+ // A refinement is a runtime predicate with no schema equivalent; the
413
+ // inner shape is still the right description of the data.
414
+ return convertInner(schema["_inner"], state, "refine");
415
+ case "transform":
416
+ return convertInner(schema["_base"], state, "transform");
417
+ case "lazy": {
418
+ const resolved = schema["_inner"] ?? resolveLazy(schema, state);
419
+ if (resolved === undefined)
420
+ return {};
421
+ return convertNode(resolved, state);
422
+ }
423
+ default:
424
+ state.warnings.push(`Unsupported schema type: ${schema._type}`);
425
+ return {};
426
+ }
427
+ }
428
+ function resolveLazy(schema, state) {
429
+ const factory = schema["_factory"];
430
+ if (typeof factory !== "function")
431
+ return undefined;
432
+ try {
433
+ return factory();
434
+ }
435
+ catch (error) {
436
+ state.warnings.push(`A \`lazy\` schema could not be resolved: ${error instanceof Error ? error.message : String(error)}`);
437
+ return undefined;
438
+ }
439
+ }
440
+ function convertInner(inner, state, wrapper) {
441
+ if (inner === undefined) {
442
+ state.warnings.push(`A \`${wrapper}\` schema has no inner schema.`);
443
+ return {};
444
+ }
445
+ return convertNode(inner, state);
446
+ }
447
+ /**
448
+ * Converts a bare JavaScript value used as a shorthand schema.
449
+ *
450
+ * `["a"]` means "array of string" and `"x"` means "string" — a convenience
451
+ * for hand-written metadata, not for real schemas.
452
+ */
453
+ function convertLiteralValue(input, state) {
454
+ if (Array.isArray(input)) {
455
+ const first = input[0];
456
+ if (first === undefined)
457
+ return { type: "array" };
458
+ return { type: "array", items: convertNode(first, state) };
129
459
  }
130
460
  if (typeof input === "string")
131
- return { schema: { type: "string" }, warnings: [] };
132
- if (typeof input === "number")
133
- return { schema: { type: "number" }, warnings: [] };
461
+ return { type: "string" };
462
+ if (typeof input === "number") {
463
+ return { type: Number.isInteger(input) ? "integer" : "number" };
464
+ }
134
465
  if (typeof input === "boolean")
135
- return { schema: { type: "boolean" }, warnings: [] };
136
- return { schema: {}, warnings: ["Unable to convert unknown schema input."] };
466
+ return { type: "boolean" };
467
+ if (input === null) {
468
+ return isVersion31(state.version) ? { type: "null" } : { nullable: true };
469
+ }
470
+ state.warnings.push("Unable to convert unknown schema input.");
471
+ return {};
472
+ }
473
+ /**
474
+ * Converts a schema into an OpenAPI schema object.
475
+ *
476
+ * Warnings describe everything that could not be represented exactly. They
477
+ * are part of the result rather than a side channel, because silently
478
+ * emitting `{}` for an unsupported construct is how a specification ends up
479
+ * documenting nothing.
480
+ */
481
+ export function convertSchema(input, options) {
482
+ const state = {
483
+ warnings: [],
484
+ visited: new Set(),
485
+ version: options?.version ?? DEFAULT_OPENAPI_VERSION,
486
+ maxDepth: options?.maxDepth ?? 32,
487
+ depth: 0,
488
+ };
489
+ const schema = convertNode(input, state);
490
+ return { schema, warnings: state.warnings };
137
491
  }
138
- export function createSchemaConverter() {
139
- return { convert: convertSchema };
492
+ /** Creates a converter bound to a specification version. */
493
+ export function createSchemaConverter(defaults) {
494
+ return {
495
+ convert: (input, options) => convertSchema(input, { ...defaults, ...options }),
496
+ };
140
497
  }
141
498
  //# sourceMappingURL=schemaConverter.core.js.map