@kubb/adapter-oas 5.0.0-beta.9 → 5.0.0-beta.91
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/LICENSE +17 -10
- package/README.md +26 -24
- package/dist/index.cjs +1284 -848
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +846 -79
- package/dist/index.js +1280 -839
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/extension.yaml +0 -344
- package/src/adapter.ts +0 -130
- package/src/constants.ts +0 -122
- package/src/discriminator.ts +0 -108
- package/src/factory.ts +0 -162
- package/src/guards.ts +0 -68
- package/src/index.ts +0 -18
- package/src/parser.ts +0 -1002
- package/src/refs.ts +0 -74
- package/src/resolvers.ts +0 -544
- package/src/types.ts +0 -206
- /package/dist/{chunk--u3MIqq1.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -21,28 +21,24 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
+
let _kubb_ast = require("@kubb/ast");
|
|
24
25
|
let _kubb_core = require("@kubb/core");
|
|
25
26
|
let node_path = require("node:path");
|
|
26
27
|
node_path = __toESM(node_path, 1);
|
|
27
|
-
let
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
let oas = require("oas");
|
|
33
|
-
oas = __toESM(oas, 1);
|
|
34
|
-
let oas_types = require("oas/types");
|
|
35
|
-
let oas_utils = require("oas/utils");
|
|
28
|
+
let node_fs_promises = require("node:fs/promises");
|
|
29
|
+
let _readme_openapi_parser = require("@readme/openapi-parser");
|
|
30
|
+
let _scalar_openapi_upgrader = require("@scalar/openapi-upgrader");
|
|
31
|
+
let yaml = require("yaml");
|
|
32
|
+
let api_ref_bundler = require("api-ref-bundler");
|
|
36
33
|
//#region src/constants.ts
|
|
37
34
|
/**
|
|
38
35
|
* Default parser options applied when no explicit options are provided.
|
|
39
36
|
*
|
|
40
37
|
* @example
|
|
41
38
|
* ```ts
|
|
42
|
-
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
39
|
+
* import { DEFAULT_PARSER_OPTIONS, parseOas } from '@kubb/adapter-oas'
|
|
43
40
|
*
|
|
44
|
-
* const
|
|
45
|
-
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
41
|
+
* const { root } = parseOas(document, { ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
46
42
|
* ```
|
|
47
43
|
*/
|
|
48
44
|
const DEFAULT_PARSER_OPTIONS = {
|
|
@@ -64,17 +60,19 @@ const DEFAULT_PARSER_OPTIONS = {
|
|
|
64
60
|
*/
|
|
65
61
|
const SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
66
62
|
/**
|
|
67
|
-
*
|
|
63
|
+
* HTTP methods that count as operations on an OpenAPI path item. Other keys
|
|
64
|
+
* (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
|
|
68
65
|
*/
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
67
|
+
"get",
|
|
68
|
+
"put",
|
|
69
|
+
"post",
|
|
70
|
+
"delete",
|
|
71
|
+
"options",
|
|
72
|
+
"head",
|
|
73
|
+
"patch",
|
|
74
|
+
"trace"
|
|
75
|
+
]);
|
|
78
76
|
/**
|
|
79
77
|
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
80
78
|
*
|
|
@@ -89,7 +87,7 @@ const MERGE_DEFAULT_VERSION = "1.0.0";
|
|
|
89
87
|
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
90
88
|
* ```
|
|
91
89
|
*/
|
|
92
|
-
const structuralKeys = new Set([
|
|
90
|
+
const structuralKeys = /* @__PURE__ */ new Set([
|
|
93
91
|
"properties",
|
|
94
92
|
"items",
|
|
95
93
|
"additionalProperties",
|
|
@@ -99,12 +97,24 @@ const structuralKeys = new Set([
|
|
|
99
97
|
"not"
|
|
100
98
|
]);
|
|
101
99
|
/**
|
|
100
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
101
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
102
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
103
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
104
|
+
*/
|
|
105
|
+
const specialCasedFormats = /* @__PURE__ */ new Set([
|
|
106
|
+
"int64",
|
|
107
|
+
"date-time",
|
|
108
|
+
"date",
|
|
109
|
+
"time"
|
|
110
|
+
]);
|
|
111
|
+
/**
|
|
102
112
|
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
103
113
|
*
|
|
104
114
|
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
105
|
-
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
|
|
106
|
-
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types
|
|
107
|
-
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
115
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
|
|
116
|
+
* separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
|
|
117
|
+
* and `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
108
118
|
*
|
|
109
119
|
* @example
|
|
110
120
|
* ```ts
|
|
@@ -144,39 +154,33 @@ const formatMap = {
|
|
|
144
154
|
*/
|
|
145
155
|
const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
146
156
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
157
|
+
* Vendor extension keys that attach human-readable descriptions to enum values, checked in priority order.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { enumDescriptionKeys } from '@kubb/adapter-oas'
|
|
162
|
+
*
|
|
163
|
+
* const key = enumDescriptionKeys.find((k) => k in schema) // 'x-enumDescriptions' | 'x-enum-descriptions' | undefined
|
|
164
|
+
* ```
|
|
149
165
|
*/
|
|
150
|
-
const
|
|
151
|
-
["any", _kubb_core.ast.schemaTypes.any],
|
|
152
|
-
["unknown", _kubb_core.ast.schemaTypes.unknown],
|
|
153
|
-
["void", _kubb_core.ast.schemaTypes.void]
|
|
154
|
-
]);
|
|
166
|
+
const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
|
|
155
167
|
//#endregion
|
|
156
168
|
//#region src/discriminator.ts
|
|
157
169
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
161
|
-
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
162
|
-
* child object schema.
|
|
170
|
+
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
171
|
+
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
163
172
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* ```ts
|
|
168
|
-
* const { root } = parseOas(document, options)
|
|
169
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
170
|
-
* ```
|
|
173
|
+
* Called on a small pre-parsed subset of schemas (only the discriminator parents)
|
|
174
|
+
* rather than on all schemas at once.
|
|
171
175
|
*/
|
|
172
|
-
function
|
|
176
|
+
function buildDiscriminatorChildMap(schemas) {
|
|
173
177
|
const childMap = /* @__PURE__ */ new Map();
|
|
174
|
-
for (const schema of
|
|
175
|
-
let unionNode =
|
|
178
|
+
for (const schema of schemas) {
|
|
179
|
+
let unionNode = _kubb_ast.ast.narrowSchema(schema, "union");
|
|
176
180
|
if (!unionNode) {
|
|
177
|
-
const intersectionMembers =
|
|
181
|
+
const intersectionMembers = _kubb_ast.ast.narrowSchema(schema, "intersection")?.members;
|
|
178
182
|
if (intersectionMembers) for (const m of intersectionMembers) {
|
|
179
|
-
const u =
|
|
183
|
+
const u = _kubb_ast.ast.narrowSchema(m, "union");
|
|
180
184
|
if (u) {
|
|
181
185
|
unionNode = u;
|
|
182
186
|
break;
|
|
@@ -186,416 +190,337 @@ function applyDiscriminatorInheritance(root) {
|
|
|
186
190
|
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
|
|
187
191
|
const { discriminatorPropertyName, members } = unionNode;
|
|
188
192
|
for (const member of members) {
|
|
189
|
-
const intersectionNode =
|
|
193
|
+
const intersectionNode = _kubb_ast.ast.narrowSchema(member, "intersection");
|
|
190
194
|
if (!intersectionNode?.members) continue;
|
|
191
|
-
let refNode;
|
|
192
|
-
let objNode;
|
|
195
|
+
let refNode = null;
|
|
196
|
+
let objNode = null;
|
|
193
197
|
for (const m of intersectionNode.members) {
|
|
194
|
-
refNode ??=
|
|
195
|
-
objNode ??=
|
|
198
|
+
refNode ??= _kubb_ast.ast.narrowSchema(m, "ref");
|
|
199
|
+
objNode ??= _kubb_ast.ast.narrowSchema(m, "object");
|
|
196
200
|
}
|
|
197
201
|
if (!refNode?.name || !objNode) continue;
|
|
198
202
|
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
|
|
199
|
-
const enumNode = prop ?
|
|
203
|
+
const enumNode = prop ? _kubb_ast.ast.narrowSchema(prop.schema, "enum") : null;
|
|
200
204
|
if (!enumNode?.enumValues?.length) continue;
|
|
201
205
|
const enumValues = enumNode.enumValues.filter((v) => v !== null);
|
|
202
206
|
if (!enumValues.length) continue;
|
|
203
207
|
const existing = childMap.get(refNode.name);
|
|
204
|
-
if (existing)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
if (!existing) {
|
|
209
|
+
childMap.set(refNode.name, {
|
|
210
|
+
propertyName: discriminatorPropertyName,
|
|
211
|
+
enumValues: [...enumValues]
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
existing.enumValues.push(...enumValues);
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
|
-
|
|
212
|
-
return _kubb_core.ast.transform(root, { schema(node, { parent }) {
|
|
213
|
-
if (parent?.kind !== "Input" || !node.name) return;
|
|
214
|
-
const entry = childMap.get(node.name);
|
|
215
|
-
if (!entry) return;
|
|
216
|
-
const objectNode = _kubb_core.ast.narrowSchema(node, "object");
|
|
217
|
-
if (!objectNode) return;
|
|
218
|
-
const { propertyName, enumValues } = entry;
|
|
219
|
-
const enumSchema = _kubb_core.ast.createSchema({
|
|
220
|
-
type: "enum",
|
|
221
|
-
enumValues
|
|
222
|
-
});
|
|
223
|
-
const newProp = _kubb_core.ast.createProperty({
|
|
224
|
-
name: propertyName,
|
|
225
|
-
required: true,
|
|
226
|
-
schema: enumSchema
|
|
227
|
-
});
|
|
228
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
|
|
229
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
|
|
230
|
-
return {
|
|
231
|
-
...objectNode,
|
|
232
|
-
properties: newProperties
|
|
233
|
-
};
|
|
234
|
-
} });
|
|
218
|
+
return childMap;
|
|
235
219
|
}
|
|
236
|
-
//#endregion
|
|
237
|
-
//#region ../../internals/utils/src/casing.ts
|
|
238
220
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* and capitalizes each word according to `pascal`.
|
|
242
|
-
*
|
|
243
|
-
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
244
|
-
*/
|
|
245
|
-
function toCamelOrPascal(text, pascal) {
|
|
246
|
-
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
247
|
-
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
248
|
-
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
249
|
-
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
250
|
-
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Splits `text` on `.` and applies `transformPart` to each segment.
|
|
254
|
-
* The last segment receives `isLast = true`, all earlier segments receive `false`.
|
|
255
|
-
* Segments are joined with `/` to form a file path.
|
|
256
|
-
*
|
|
257
|
-
* Only splits on dots followed by a letter so that version numbers
|
|
258
|
-
* embedded in operationIds (e.g. `v2025.0`) are kept intact.
|
|
259
|
-
*
|
|
260
|
-
* Empty segments are filtered before joining. They arise when the text starts with
|
|
261
|
-
* a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
|
|
262
|
-
* and `'..'` transforms to an empty string). Without this filter the join would produce
|
|
263
|
-
* a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
|
|
264
|
-
* generated files to escape the configured output directory.
|
|
221
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
222
|
+
* the discriminant property).
|
|
265
223
|
*/
|
|
266
|
-
function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Converts `text` to PascalCase.
|
|
287
|
-
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* pascalCase('hello-world') // 'HelloWorld'
|
|
291
|
-
* pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
|
|
292
|
-
*/
|
|
293
|
-
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
294
|
-
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
295
|
-
prefix,
|
|
296
|
-
suffix
|
|
297
|
-
}) : camelCase(part));
|
|
298
|
-
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
224
|
+
function patchDiscriminatorNode(node, entry) {
|
|
225
|
+
const objectNode = _kubb_ast.ast.narrowSchema(node, "object");
|
|
226
|
+
if (!objectNode) return node;
|
|
227
|
+
const { propertyName, enumValues } = entry;
|
|
228
|
+
const enumSchema = _kubb_ast.ast.factory.createSchema({
|
|
229
|
+
type: "enum",
|
|
230
|
+
enumValues
|
|
231
|
+
});
|
|
232
|
+
const newProp = _kubb_ast.ast.factory.createProperty({
|
|
233
|
+
name: propertyName,
|
|
234
|
+
required: true,
|
|
235
|
+
schema: enumSchema
|
|
236
|
+
});
|
|
237
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
|
|
238
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
|
|
239
|
+
return {
|
|
240
|
+
...objectNode,
|
|
241
|
+
properties: newProperties
|
|
242
|
+
};
|
|
299
243
|
}
|
|
300
|
-
//#endregion
|
|
301
|
-
//#region ../../internals/utils/src/object.ts
|
|
302
244
|
/**
|
|
303
|
-
*
|
|
245
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
304
246
|
*
|
|
305
247
|
* @example
|
|
306
248
|
* ```ts
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* isPlainObject(null) // false
|
|
249
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
250
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
310
251
|
* ```
|
|
311
252
|
*/
|
|
312
|
-
function
|
|
313
|
-
return
|
|
253
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
254
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
255
|
+
type: "object",
|
|
256
|
+
primitive: "object",
|
|
257
|
+
properties: [_kubb_ast.ast.factory.createProperty({
|
|
258
|
+
name: propertyName,
|
|
259
|
+
schema: _kubb_ast.ast.factory.createSchema({
|
|
260
|
+
type: "enum",
|
|
261
|
+
primitive: "string",
|
|
262
|
+
enumValues: [value]
|
|
263
|
+
}),
|
|
264
|
+
required: true
|
|
265
|
+
})]
|
|
266
|
+
});
|
|
314
267
|
}
|
|
315
268
|
/**
|
|
316
|
-
*
|
|
317
|
-
* Arrays and non-object values from `source` override the corresponding values in `target`.
|
|
269
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
318
270
|
*
|
|
319
271
|
* @example
|
|
320
272
|
* ```ts
|
|
321
|
-
*
|
|
322
|
-
* // { a: { x: 1, y: 2 } }
|
|
273
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
323
274
|
* ```
|
|
324
275
|
*/
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const sv = source[key];
|
|
329
|
-
const tv = result[key];
|
|
330
|
-
result[key] = sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv) ? mergeDeep(tv, sv) : sv;
|
|
331
|
-
}
|
|
332
|
-
return result;
|
|
276
|
+
function findDiscriminator(mapping, ref) {
|
|
277
|
+
if (!mapping || !ref) return null;
|
|
278
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
333
279
|
}
|
|
334
280
|
//#endregion
|
|
335
|
-
//#region ../../internals/utils/src/
|
|
336
|
-
/**
|
|
337
|
-
* JavaScript and Java reserved words.
|
|
338
|
-
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
339
|
-
*/
|
|
340
|
-
const reservedWords = new Set([
|
|
341
|
-
"abstract",
|
|
342
|
-
"arguments",
|
|
343
|
-
"boolean",
|
|
344
|
-
"break",
|
|
345
|
-
"byte",
|
|
346
|
-
"case",
|
|
347
|
-
"catch",
|
|
348
|
-
"char",
|
|
349
|
-
"class",
|
|
350
|
-
"const",
|
|
351
|
-
"continue",
|
|
352
|
-
"debugger",
|
|
353
|
-
"default",
|
|
354
|
-
"delete",
|
|
355
|
-
"do",
|
|
356
|
-
"double",
|
|
357
|
-
"else",
|
|
358
|
-
"enum",
|
|
359
|
-
"eval",
|
|
360
|
-
"export",
|
|
361
|
-
"extends",
|
|
362
|
-
"false",
|
|
363
|
-
"final",
|
|
364
|
-
"finally",
|
|
365
|
-
"float",
|
|
366
|
-
"for",
|
|
367
|
-
"function",
|
|
368
|
-
"goto",
|
|
369
|
-
"if",
|
|
370
|
-
"implements",
|
|
371
|
-
"import",
|
|
372
|
-
"in",
|
|
373
|
-
"instanceof",
|
|
374
|
-
"int",
|
|
375
|
-
"interface",
|
|
376
|
-
"let",
|
|
377
|
-
"long",
|
|
378
|
-
"native",
|
|
379
|
-
"new",
|
|
380
|
-
"null",
|
|
381
|
-
"package",
|
|
382
|
-
"private",
|
|
383
|
-
"protected",
|
|
384
|
-
"public",
|
|
385
|
-
"return",
|
|
386
|
-
"short",
|
|
387
|
-
"static",
|
|
388
|
-
"super",
|
|
389
|
-
"switch",
|
|
390
|
-
"synchronized",
|
|
391
|
-
"this",
|
|
392
|
-
"throw",
|
|
393
|
-
"throws",
|
|
394
|
-
"transient",
|
|
395
|
-
"true",
|
|
396
|
-
"try",
|
|
397
|
-
"typeof",
|
|
398
|
-
"var",
|
|
399
|
-
"void",
|
|
400
|
-
"volatile",
|
|
401
|
-
"while",
|
|
402
|
-
"with",
|
|
403
|
-
"yield",
|
|
404
|
-
"Array",
|
|
405
|
-
"Date",
|
|
406
|
-
"hasOwnProperty",
|
|
407
|
-
"Infinity",
|
|
408
|
-
"isFinite",
|
|
409
|
-
"isNaN",
|
|
410
|
-
"isPrototypeOf",
|
|
411
|
-
"length",
|
|
412
|
-
"Math",
|
|
413
|
-
"name",
|
|
414
|
-
"NaN",
|
|
415
|
-
"Number",
|
|
416
|
-
"Object",
|
|
417
|
-
"prototype",
|
|
418
|
-
"String",
|
|
419
|
-
"toString",
|
|
420
|
-
"undefined",
|
|
421
|
-
"valueOf"
|
|
422
|
-
]);
|
|
281
|
+
//#region ../../internals/utils/src/casing.ts
|
|
423
282
|
/**
|
|
424
|
-
*
|
|
283
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
284
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
285
|
+
* and capitalizes each word according to `pascal`.
|
|
425
286
|
*
|
|
426
|
-
*
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
287
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
288
|
+
*/
|
|
289
|
+
function toCamelOrPascal(text, pascal) {
|
|
290
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
291
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
292
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
293
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Converts `text` to PascalCase.
|
|
297
|
+
*
|
|
298
|
+
* @example Word boundaries
|
|
299
|
+
* `pascalCase('hello-world') // 'HelloWorld'`
|
|
300
|
+
*
|
|
301
|
+
* @example With a suffix
|
|
302
|
+
* `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
|
|
432
303
|
*/
|
|
433
|
-
function
|
|
434
|
-
|
|
435
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
304
|
+
function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
305
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
436
306
|
}
|
|
437
307
|
//#endregion
|
|
438
|
-
//#region ../../internals/utils/src/
|
|
308
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
439
309
|
/**
|
|
440
|
-
*
|
|
310
|
+
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
441
311
|
*
|
|
442
|
-
* @
|
|
443
|
-
* const p = new URLPath('/pet/{petId}')
|
|
444
|
-
* p.URL // '/pet/:petId'
|
|
445
|
-
* p.template // '`/pet/${petId}`'
|
|
312
|
+
* Prefer the shared {@link runtime} instance over constructing your own.
|
|
446
313
|
*/
|
|
447
|
-
var
|
|
314
|
+
var Runtime = class {
|
|
448
315
|
/**
|
|
449
|
-
*
|
|
450
|
-
*/
|
|
451
|
-
path;
|
|
452
|
-
#options;
|
|
453
|
-
constructor(path, options = {}) {
|
|
454
|
-
this.path = path;
|
|
455
|
-
this.#options = options;
|
|
456
|
-
}
|
|
457
|
-
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
316
|
+
* `true` when the current process is running under Bun.
|
|
458
317
|
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
|
-
get URL() {
|
|
465
|
-
return this.toURLPath();
|
|
466
|
-
}
|
|
467
|
-
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
|
|
318
|
+
* Detection keys off the global `Bun` object rather than `process.versions`,
|
|
319
|
+
* because Bun polyfills `process.versions.node` for Node compatibility and would
|
|
320
|
+
* otherwise look like Node.
|
|
468
321
|
*
|
|
469
322
|
* @example
|
|
470
323
|
* ```ts
|
|
471
|
-
*
|
|
472
|
-
*
|
|
324
|
+
* if (runtime.isBun) {
|
|
325
|
+
* await Bun.write(path, data)
|
|
326
|
+
* }
|
|
473
327
|
* ```
|
|
474
328
|
*/
|
|
475
|
-
get
|
|
476
|
-
|
|
477
|
-
return !!new URL(this.path).href;
|
|
478
|
-
} catch {
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
329
|
+
get isBun() {
|
|
330
|
+
return typeof Bun !== "undefined";
|
|
481
331
|
}
|
|
482
332
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* @example
|
|
486
|
-
* new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
|
|
487
|
-
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
|
|
488
|
-
*/
|
|
489
|
-
get template() {
|
|
490
|
-
return this.toTemplateString();
|
|
491
|
-
}
|
|
492
|
-
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* ```ts
|
|
496
|
-
* new URLPath('/pet/{petId}').object
|
|
497
|
-
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
get object() {
|
|
501
|
-
return this.toObject();
|
|
502
|
-
}
|
|
503
|
-
/** Returns a map of path parameter names, or `undefined` when the path has no parameters.
|
|
504
|
-
*
|
|
505
|
-
* @example
|
|
506
|
-
* ```ts
|
|
507
|
-
* new URLPath('/pet/{petId}').params // { petId: 'petId' }
|
|
508
|
-
* new URLPath('/pet').params // undefined
|
|
509
|
-
* ```
|
|
333
|
+
* `true` when the current process is running under Deno.
|
|
510
334
|
*/
|
|
511
|
-
get
|
|
512
|
-
return
|
|
513
|
-
}
|
|
514
|
-
#transformParam(raw) {
|
|
515
|
-
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
516
|
-
return this.#options.casing === "camelcase" ? camelCase(param) : param;
|
|
335
|
+
get isDeno() {
|
|
336
|
+
return typeof globalThis.Deno !== "undefined";
|
|
517
337
|
}
|
|
518
338
|
/**
|
|
519
|
-
*
|
|
520
|
-
*/
|
|
521
|
-
#eachParam(fn) {
|
|
522
|
-
for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
|
|
523
|
-
const raw = match[1];
|
|
524
|
-
fn(raw, this.#transformParam(raw));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
toObject({ type = "path", replacer, stringify } = {}) {
|
|
528
|
-
const object = {
|
|
529
|
-
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
|
|
530
|
-
params: this.getParams()
|
|
531
|
-
};
|
|
532
|
-
if (stringify) {
|
|
533
|
-
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
534
|
-
if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
|
|
535
|
-
return `{ url: '${object.url}' }`;
|
|
536
|
-
}
|
|
537
|
-
return object;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Converts the OpenAPI path to a TypeScript template literal string.
|
|
541
|
-
* An optional `replacer` can transform each extracted parameter name before interpolation.
|
|
339
|
+
* `true` when the current process is running under Node.
|
|
542
340
|
*
|
|
543
|
-
*
|
|
544
|
-
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
341
|
+
* Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
|
|
545
342
|
*/
|
|
546
|
-
|
|
547
|
-
return
|
|
548
|
-
if (i % 2 === 0) return part;
|
|
549
|
-
const param = this.#transformParam(part);
|
|
550
|
-
return `\${${replacer ? replacer(param) : param}}`;
|
|
551
|
-
}).join("")}\``;
|
|
343
|
+
get isNode() {
|
|
344
|
+
return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
|
|
552
345
|
}
|
|
553
346
|
/**
|
|
554
|
-
*
|
|
555
|
-
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
556
|
-
* Returns `undefined` when no path parameters are found.
|
|
347
|
+
* Name of the runtime executing the current process.
|
|
557
348
|
*
|
|
558
349
|
* @example
|
|
559
350
|
* ```ts
|
|
560
|
-
*
|
|
561
|
-
* // { petId: 'petId', tagId: 'tagId' }
|
|
351
|
+
* runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
|
|
562
352
|
* ```
|
|
563
353
|
*/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this
|
|
567
|
-
|
|
568
|
-
params[key] = key;
|
|
569
|
-
});
|
|
570
|
-
return Object.keys(params).length > 0 ? params : void 0;
|
|
354
|
+
get name() {
|
|
355
|
+
if (this.isBun) return "bun";
|
|
356
|
+
if (this.isDeno) return "deno";
|
|
357
|
+
return "node";
|
|
571
358
|
}
|
|
572
|
-
/**
|
|
359
|
+
/**
|
|
360
|
+
* Version of the active runtime, or an empty string when it cannot be read.
|
|
573
361
|
*
|
|
574
362
|
* @example
|
|
575
363
|
* ```ts
|
|
576
|
-
*
|
|
364
|
+
* runtime.version // '1.3.11' under Bun, '22.22.2' under Node
|
|
577
365
|
* ```
|
|
578
366
|
*/
|
|
579
|
-
|
|
580
|
-
|
|
367
|
+
get version() {
|
|
368
|
+
if (this.isBun) return process.versions.bun ?? "";
|
|
369
|
+
if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
|
|
370
|
+
return process.versions?.node ?? "";
|
|
581
371
|
}
|
|
582
372
|
};
|
|
373
|
+
/**
|
|
374
|
+
* Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
|
|
375
|
+
*/
|
|
376
|
+
const runtime = new Runtime();
|
|
583
377
|
//#endregion
|
|
584
|
-
//#region src/
|
|
378
|
+
//#region ../../internals/utils/src/fs.ts
|
|
585
379
|
/**
|
|
586
|
-
*
|
|
380
|
+
* Resolves to `true` when the file or directory at `path` exists.
|
|
381
|
+
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
587
382
|
*
|
|
588
383
|
* @example
|
|
589
384
|
* ```ts
|
|
590
|
-
* if (
|
|
591
|
-
*
|
|
385
|
+
* if (await exists('./kubb.config.ts')) {
|
|
386
|
+
* const content = await read('./kubb.config.ts')
|
|
592
387
|
* }
|
|
593
388
|
* ```
|
|
594
389
|
*/
|
|
595
|
-
function
|
|
596
|
-
|
|
390
|
+
async function exists(path) {
|
|
391
|
+
if (runtime.isBun) return Bun.file(path).exists();
|
|
392
|
+
return (0, node_fs_promises.access)(path).then(() => true, () => false);
|
|
597
393
|
}
|
|
598
394
|
/**
|
|
395
|
+
* Reads the file at `path` as a UTF-8 string.
|
|
396
|
+
* Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```ts
|
|
400
|
+
* const source = await read('./src/Pet.ts')
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
async function read(path) {
|
|
404
|
+
if (runtime.isBun) return Bun.file(path).text();
|
|
405
|
+
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/bundler.ts
|
|
409
|
+
const urlRegExp = /^https?:\/+/i;
|
|
410
|
+
async function readSource(sourcePath) {
|
|
411
|
+
if (urlRegExp.test(sourcePath)) {
|
|
412
|
+
const url = new URL(sourcePath);
|
|
413
|
+
const response = await fetch(url);
|
|
414
|
+
if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
|
|
415
|
+
return response.text();
|
|
416
|
+
}
|
|
417
|
+
return read(sourcePath);
|
|
418
|
+
}
|
|
419
|
+
async function resolveSource(sourcePath) {
|
|
420
|
+
const data = await readSource(sourcePath);
|
|
421
|
+
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
422
|
+
return (0, yaml.parse)(data);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
426
|
+
*
|
|
427
|
+
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
428
|
+
* pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
|
|
429
|
+
* can then emit a named type with an import instead of inlining the shape. Sources are read with
|
|
430
|
+
* the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
|
|
431
|
+
*
|
|
432
|
+
* @example Local file
|
|
433
|
+
* `const document = await bundleDocument('./openapi.yaml')`
|
|
434
|
+
*
|
|
435
|
+
* @example Remote URL
|
|
436
|
+
* `const document = await bundleDocument('https://example.com/openapi.yaml')`
|
|
437
|
+
*/
|
|
438
|
+
async function bundleDocument(pathOrUrl) {
|
|
439
|
+
const cache = /* @__PURE__ */ new Map();
|
|
440
|
+
const resolver = (sourcePath) => {
|
|
441
|
+
const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
|
|
442
|
+
const cached = cache.get(key);
|
|
443
|
+
if (cached) return cached;
|
|
444
|
+
const result = resolveSource(sourcePath);
|
|
445
|
+
cache.set(key, result);
|
|
446
|
+
return result;
|
|
447
|
+
};
|
|
448
|
+
await resolver(pathOrUrl);
|
|
449
|
+
return await (0, api_ref_bundler.bundle)(pathOrUrl, resolver);
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/factory.ts
|
|
453
|
+
/**
|
|
454
|
+
* Loads and bundles an OpenAPI document, returning the raw `Document`.
|
|
455
|
+
*
|
|
456
|
+
* A string is a file path or URL: it is bundled via `api-ref-bundler`, hoisting external file
|
|
457
|
+
* schemas into named `components.schemas` entries so generators can emit named types and imports.
|
|
458
|
+
* An object is treated as an already-parsed document. Swagger 2.0 and OpenAPI 3.0 documents are
|
|
459
|
+
* up-converted to OpenAPI 3.1 via `@scalar/openapi-upgrader`.
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```ts
|
|
463
|
+
* const document = await parseDocument('./openapi.yaml')
|
|
464
|
+
* const document = await parseDocument(rawDocumentObject)
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
async function parseDocument(pathOrApi) {
|
|
468
|
+
if (typeof pathOrApi === "string") return parseDocument(await bundleDocument(pathOrApi));
|
|
469
|
+
return (0, _scalar_openapi_upgrader.upgrade)(pathOrApi, "3.1");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Creates a `Document` from an `AdapterSource`.
|
|
473
|
+
*
|
|
474
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
475
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
480
|
+
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
async function parseFromConfig(source) {
|
|
484
|
+
if (source.type === "data") return parseDocument(typeof source.data === "string" ? (0, yaml.parse)(source.data) : structuredClone(source.data));
|
|
485
|
+
if (URL.canParse(source.path)) return parseDocument(source.path);
|
|
486
|
+
const resolved = node_path.default.resolve(node_path.default.dirname(source.path), source.path);
|
|
487
|
+
await assertInputExists(resolved);
|
|
488
|
+
return parseDocument(resolved);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
492
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
493
|
+
* its parse error instead.
|
|
494
|
+
*/
|
|
495
|
+
async function assertInputExists(input) {
|
|
496
|
+
if (URL.canParse(input)) return;
|
|
497
|
+
if (!await exists(input)) throw new _kubb_core.Diagnostics.Error({
|
|
498
|
+
code: _kubb_core.Diagnostics.code.inputNotFound,
|
|
499
|
+
severity: "error",
|
|
500
|
+
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
501
|
+
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
502
|
+
location: { kind: "config" }
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```ts
|
|
510
|
+
* await validateDocument(document)
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
514
|
+
try {
|
|
515
|
+
const result = await (0, _readme_openapi_parser.validate)(structuredClone(document), { validate: { errors: { colorize: true } } });
|
|
516
|
+
if (!result.valid) throw new Error((0, _readme_openapi_parser.compileErrors)(result));
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (throwOnError) throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/guards.ts
|
|
523
|
+
/**
|
|
599
524
|
* Returns `true` when a schema should be treated as nullable.
|
|
600
525
|
*
|
|
601
526
|
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
@@ -641,107 +566,32 @@ function isDiscriminator(obj) {
|
|
|
641
566
|
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
642
567
|
}
|
|
643
568
|
//#endregion
|
|
644
|
-
//#region src/
|
|
569
|
+
//#region src/mime.ts
|
|
645
570
|
/**
|
|
646
|
-
*
|
|
571
|
+
* MIME type fragments that mark a media type as JSON-like.
|
|
647
572
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* to OpenAPI 3.0 via `swagger2openapi`.
|
|
651
|
-
*
|
|
652
|
-
* @example
|
|
653
|
-
* ```ts
|
|
654
|
-
* const document = await parseDocument('./openapi.yaml')
|
|
655
|
-
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
656
|
-
* ```
|
|
573
|
+
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
574
|
+
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
657
575
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
enablePaths
|
|
666
|
-
});
|
|
667
|
-
const document = await new oas_normalize.default(pathOrApi, {
|
|
668
|
-
enablePaths,
|
|
669
|
-
colorizeErrors: true
|
|
670
|
-
}).load();
|
|
671
|
-
if (isOpenApiV2Document(document)) {
|
|
672
|
-
const { openapi } = await swagger2openapi.default.convertObj(document, { anchors: true });
|
|
673
|
-
return openapi;
|
|
674
|
-
}
|
|
675
|
-
return document;
|
|
676
|
-
}
|
|
576
|
+
const jsonMimeFragments = [
|
|
577
|
+
"application/json",
|
|
578
|
+
"application/x-json",
|
|
579
|
+
"text/json",
|
|
580
|
+
"text/x-json",
|
|
581
|
+
"+json"
|
|
582
|
+
];
|
|
677
583
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
|
|
681
|
-
* Throws when the input array is empty.
|
|
584
|
+
* Returns `true` when a media type string is JSON-like.
|
|
682
585
|
*
|
|
683
586
|
* @example
|
|
684
587
|
* ```ts
|
|
685
|
-
*
|
|
588
|
+
* isJsonMimeType('application/json') // true
|
|
589
|
+
* isJsonMimeType('application/vnd.api+json') // true
|
|
590
|
+
* isJsonMimeType('multipart/form-data') // false
|
|
686
591
|
* ```
|
|
687
592
|
*/
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
enablePaths: false,
|
|
691
|
-
canBundle: false
|
|
692
|
-
})));
|
|
693
|
-
if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
|
|
694
|
-
const seed = {
|
|
695
|
-
openapi: MERGE_OPENAPI_VERSION,
|
|
696
|
-
info: {
|
|
697
|
-
title: MERGE_DEFAULT_TITLE,
|
|
698
|
-
version: MERGE_DEFAULT_VERSION
|
|
699
|
-
},
|
|
700
|
-
paths: {},
|
|
701
|
-
components: { schemas: {} }
|
|
702
|
-
};
|
|
703
|
-
return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Creates a `Document` from an `AdapterSource`.
|
|
707
|
-
*
|
|
708
|
-
* Handles all three source types:
|
|
709
|
-
* - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
|
|
710
|
-
* - `{ type: 'paths' }` — merges multiple file paths into a single document.
|
|
711
|
-
* - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
|
|
712
|
-
*
|
|
713
|
-
* @example
|
|
714
|
-
* ```ts
|
|
715
|
-
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
716
|
-
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
717
|
-
* ```
|
|
718
|
-
*/
|
|
719
|
-
function parseFromConfig(source) {
|
|
720
|
-
if (source.type === "data") {
|
|
721
|
-
if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
|
|
722
|
-
return parseDocument(source.data, { canBundle: false });
|
|
723
|
-
}
|
|
724
|
-
if (source.type === "paths") return mergeDocuments(source.paths);
|
|
725
|
-
if (new URLPath(source.path).isURL) return parseDocument(source.path);
|
|
726
|
-
return parseDocument(node_path.default.resolve(node_path.default.dirname(source.path), source.path));
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
|
-
* Validates an OpenAPI document using `oas-normalize` with colorized error output.
|
|
730
|
-
*
|
|
731
|
-
* @example
|
|
732
|
-
* ```ts
|
|
733
|
-
* await validateDocument(document)
|
|
734
|
-
* ```
|
|
735
|
-
*/
|
|
736
|
-
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
737
|
-
try {
|
|
738
|
-
await new oas_normalize.default(document, {
|
|
739
|
-
enablePaths: true,
|
|
740
|
-
colorizeErrors: true
|
|
741
|
-
}).validate({ parser: { validate: { errors: { colorize: true } } } });
|
|
742
|
-
} catch (error) {
|
|
743
|
-
if (throwOnError) throw error;
|
|
744
|
-
}
|
|
593
|
+
function isJsonMimeType(mimeType) {
|
|
594
|
+
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
745
595
|
}
|
|
746
596
|
//#endregion
|
|
747
597
|
//#region src/refs.ts
|
|
@@ -749,20 +599,21 @@ const _refCache = /* @__PURE__ */ new WeakMap();
|
|
|
749
599
|
/**
|
|
750
600
|
* Resolves a local JSON pointer reference from a document.
|
|
751
601
|
*
|
|
752
|
-
* Accepts `#/...` refs. Returns `null` for empty or non-local
|
|
753
|
-
*
|
|
602
|
+
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
603
|
+
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
604
|
+
* build there is no sink to collect it, so it throws instead.
|
|
754
605
|
*
|
|
755
606
|
* @example
|
|
756
607
|
* ```ts
|
|
757
|
-
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
608
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
758
609
|
* ```
|
|
759
610
|
*/
|
|
760
611
|
function resolveRef(document, $ref) {
|
|
761
612
|
const origRef = $ref;
|
|
762
613
|
$ref = $ref.trim();
|
|
763
614
|
if ($ref === "") return null;
|
|
764
|
-
if (
|
|
765
|
-
|
|
615
|
+
if (!$ref.startsWith("#")) return null;
|
|
616
|
+
$ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
766
617
|
let docCache = _refCache.get(document);
|
|
767
618
|
if (!docCache) {
|
|
768
619
|
docCache = /* @__PURE__ */ new Map();
|
|
@@ -770,7 +621,21 @@ function resolveRef(document, $ref) {
|
|
|
770
621
|
}
|
|
771
622
|
if (docCache.has($ref)) return docCache.get($ref);
|
|
772
623
|
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
773
|
-
if (!current)
|
|
624
|
+
if (!current) {
|
|
625
|
+
const diagnostic = {
|
|
626
|
+
code: _kubb_core.Diagnostics.code.refNotFound,
|
|
627
|
+
severity: "error",
|
|
628
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
629
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
630
|
+
location: {
|
|
631
|
+
kind: "schema",
|
|
632
|
+
pointer: origRef,
|
|
633
|
+
ref: origRef
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
if (!_kubb_core.Diagnostics.report(diagnostic)) throw new _kubb_core.Diagnostics.Error(diagnostic);
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
774
639
|
docCache.set($ref, current);
|
|
775
640
|
return current;
|
|
776
641
|
}
|
|
@@ -795,8 +660,182 @@ function dereferenceWithRef(document, schema) {
|
|
|
795
660
|
return schema;
|
|
796
661
|
}
|
|
797
662
|
//#endregion
|
|
663
|
+
//#region src/operation.ts
|
|
664
|
+
/**
|
|
665
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
666
|
+
* with no leading or trailing dash.
|
|
667
|
+
*/
|
|
668
|
+
function slugify(value) {
|
|
669
|
+
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
673
|
+
*/
|
|
674
|
+
function getOperationId({ path, method, schema }) {
|
|
675
|
+
const { operationId } = schema;
|
|
676
|
+
if (typeof operationId === "string" && operationId.length > 0) return operationId;
|
|
677
|
+
return `${method}_${slugify(path).toLowerCase()}`;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Returns the declared response status codes, skipping `x-` extensions and non-object entries.
|
|
681
|
+
*/
|
|
682
|
+
function getResponseStatusCodes({ schema }) {
|
|
683
|
+
const responses = schema.responses;
|
|
684
|
+
if (!responses || isReference(responses)) return [];
|
|
685
|
+
return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
|
|
689
|
+
*/
|
|
690
|
+
function getResponseByStatusCode({ document, operation, statusCode }) {
|
|
691
|
+
const responses = operation.schema.responses;
|
|
692
|
+
if (!responses || isReference(responses)) return false;
|
|
693
|
+
const response = responses[statusCode];
|
|
694
|
+
if (!response) return false;
|
|
695
|
+
if (isReference(response)) {
|
|
696
|
+
const resolved = resolveRef(document, response.$ref);
|
|
697
|
+
responses[statusCode] = resolved;
|
|
698
|
+
if (!resolved || isReference(resolved)) return false;
|
|
699
|
+
return resolved;
|
|
700
|
+
}
|
|
701
|
+
return response;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
|
|
705
|
+
* `undefined` when the operation has no request body.
|
|
706
|
+
*/
|
|
707
|
+
function getRequestBodyContent({ document, operation }) {
|
|
708
|
+
const { schema } = operation;
|
|
709
|
+
let requestBody = schema.requestBody;
|
|
710
|
+
if (!requestBody) return;
|
|
711
|
+
if (isReference(requestBody)) {
|
|
712
|
+
const resolved = resolveRef(document, requestBody.$ref);
|
|
713
|
+
schema.requestBody = resolved;
|
|
714
|
+
if (!resolved || isReference(resolved)) return;
|
|
715
|
+
requestBody = resolved;
|
|
716
|
+
}
|
|
717
|
+
return requestBody.content;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Returns the request body media type. With `mediaType` set, returns that entry or `false`.
|
|
721
|
+
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
722
|
+
* `[mediaType, object]` tuple.
|
|
723
|
+
*/
|
|
724
|
+
function getRequestContent({ document, operation, mediaType }) {
|
|
725
|
+
const content = getRequestBodyContent({
|
|
726
|
+
document,
|
|
727
|
+
operation
|
|
728
|
+
});
|
|
729
|
+
if (!content) return false;
|
|
730
|
+
if (mediaType) return mediaType in content ? content[mediaType] : false;
|
|
731
|
+
const mediaTypes = Object.keys(content);
|
|
732
|
+
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
|
|
733
|
+
return available ? [available, content[available]] : false;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins
|
|
737
|
+
* when several are declared), then the first declared one, defaulting to `'application/json'`.
|
|
738
|
+
*/
|
|
739
|
+
function getRequestContentType({ document, operation }) {
|
|
740
|
+
const content = getRequestBodyContent({
|
|
741
|
+
document,
|
|
742
|
+
operation
|
|
743
|
+
});
|
|
744
|
+
const mediaTypes = content ? Object.keys(content) : [];
|
|
745
|
+
let result = mediaTypes[0] ?? "application/json";
|
|
746
|
+
for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Builds an `Operation` for every supported HTTP method on every path, in document order.
|
|
751
|
+
* `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```ts
|
|
755
|
+
* for (const operation of getOperations(document)) {
|
|
756
|
+
* parseOperation(options, operation)
|
|
757
|
+
* }
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
function getOperations(document) {
|
|
761
|
+
const operations = [];
|
|
762
|
+
const paths = document.paths;
|
|
763
|
+
if (!paths) return operations;
|
|
764
|
+
for (const path of Object.keys(paths)) {
|
|
765
|
+
if (path.startsWith("x-")) continue;
|
|
766
|
+
let pathItem = paths[path];
|
|
767
|
+
if (!pathItem) continue;
|
|
768
|
+
if (isReference(pathItem)) {
|
|
769
|
+
const resolved = resolveRef(document, pathItem.$ref);
|
|
770
|
+
paths[path] = resolved;
|
|
771
|
+
if (!resolved || isReference(resolved)) continue;
|
|
772
|
+
pathItem = resolved;
|
|
773
|
+
}
|
|
774
|
+
const item = pathItem;
|
|
775
|
+
for (const method of Object.keys(item)) {
|
|
776
|
+
if (!SUPPORTED_METHODS.has(method)) continue;
|
|
777
|
+
const schema = item[method];
|
|
778
|
+
if (!schema || typeof schema !== "object") continue;
|
|
779
|
+
operations.push({
|
|
780
|
+
path,
|
|
781
|
+
method,
|
|
782
|
+
schema
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return operations;
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/dialect.ts
|
|
790
|
+
/**
|
|
791
|
+
* The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
|
|
792
|
+
*
|
|
793
|
+
* Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
|
|
794
|
+
* decisions that differ between specs (nullability, `$ref`, discriminator, binary,
|
|
795
|
+
* ref resolution) so the converter pipeline and dispatch rules stay shared. A
|
|
796
|
+
* future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
|
|
797
|
+
* nullability, no discriminator object, binary via `contentEncoding` and reuses
|
|
798
|
+
* the rest unchanged.
|
|
799
|
+
*
|
|
800
|
+
* Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
|
|
801
|
+
* JSON Schema vocabulary, so the converters keep that common case.
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```ts
|
|
805
|
+
* const parser = createSchemaParser(context) // uses oasDialect
|
|
806
|
+
* const parser = createSchemaParser(context, oasDialect) // explicit
|
|
807
|
+
* ```
|
|
808
|
+
*/
|
|
809
|
+
const oasDialect = _kubb_ast.ast.defineDialect({
|
|
810
|
+
name: "oas",
|
|
811
|
+
schema: {
|
|
812
|
+
isNullable,
|
|
813
|
+
isReference,
|
|
814
|
+
isDiscriminator,
|
|
815
|
+
isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
|
|
816
|
+
resolveRef
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
//#endregion
|
|
798
820
|
//#region src/resolvers.ts
|
|
799
821
|
/**
|
|
822
|
+
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
823
|
+
* interpolating any `server.variables` into the URL template.
|
|
824
|
+
*
|
|
825
|
+
* Returns `null` when `server.index` is omitted or out of range.
|
|
826
|
+
*
|
|
827
|
+
* @example Resolve the first server
|
|
828
|
+
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
829
|
+
*
|
|
830
|
+
* @example Override a path variable
|
|
831
|
+
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
832
|
+
*/
|
|
833
|
+
function resolveBaseUrl({ document, server }) {
|
|
834
|
+
const index = server?.index;
|
|
835
|
+
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
836
|
+
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
800
839
|
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
801
840
|
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
802
841
|
* Throws if an override value is not in the variable's `enum` list.
|
|
@@ -816,7 +855,16 @@ function resolveServerUrl(server, overrides) {
|
|
|
816
855
|
for (const [key, variable] of Object.entries(server.variables)) {
|
|
817
856
|
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
818
857
|
if (value === void 0) continue;
|
|
819
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(
|
|
858
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new _kubb_core.Diagnostics.Error({
|
|
859
|
+
code: _kubb_core.Diagnostics.code.invalidServerVariable,
|
|
860
|
+
severity: "error",
|
|
861
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
862
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
863
|
+
location: {
|
|
864
|
+
kind: "document",
|
|
865
|
+
pointer: "#/servers"
|
|
866
|
+
}
|
|
867
|
+
});
|
|
820
868
|
url = url.replaceAll(`{${key}}`, value);
|
|
821
869
|
}
|
|
822
870
|
return url;
|
|
@@ -829,6 +877,15 @@ function getSchemaType(format) {
|
|
|
829
877
|
return formatMap[format] ?? null;
|
|
830
878
|
}
|
|
831
879
|
/**
|
|
880
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
|
|
881
|
+
* `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
|
|
882
|
+
* the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
883
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
884
|
+
*/
|
|
885
|
+
function isHandledFormat(format) {
|
|
886
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
832
889
|
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
833
890
|
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
834
891
|
*/
|
|
@@ -838,15 +895,9 @@ function getPrimitiveType(type) {
|
|
|
838
895
|
return "string";
|
|
839
896
|
}
|
|
840
897
|
/**
|
|
841
|
-
* Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
|
|
842
|
-
*/
|
|
843
|
-
function getMediaType(contentType) {
|
|
844
|
-
return Object.values(_kubb_core.ast.mediaTypes).includes(contentType) ? contentType : null;
|
|
845
|
-
}
|
|
846
|
-
/**
|
|
847
898
|
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
848
899
|
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
849
|
-
* `$ref`
|
|
900
|
+
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
850
901
|
*
|
|
851
902
|
* @example
|
|
852
903
|
* ```ts
|
|
@@ -875,7 +926,7 @@ function getResponseBody(responseBody, contentType) {
|
|
|
875
926
|
}
|
|
876
927
|
let availableContentType;
|
|
877
928
|
const contentTypes = Object.keys(body.content);
|
|
878
|
-
for (const mt of contentTypes) if (
|
|
929
|
+
for (const mt of contentTypes) if (isJsonMimeType(mt)) {
|
|
879
930
|
availableContentType = mt;
|
|
880
931
|
break;
|
|
881
932
|
}
|
|
@@ -898,15 +949,21 @@ function getResponseBody(responseBody, contentType) {
|
|
|
898
949
|
* getResponseSchema(document, operation, '4XX') // {}
|
|
899
950
|
* ```
|
|
900
951
|
*/
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
952
|
+
function resolveResponseRefs(document, operation) {
|
|
953
|
+
const responses = operation.schema.responses;
|
|
954
|
+
if (!responses) return;
|
|
955
|
+
for (const key in responses) {
|
|
956
|
+
const schema = responses[key];
|
|
957
|
+
if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
|
|
908
958
|
}
|
|
909
|
-
|
|
959
|
+
}
|
|
960
|
+
function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
961
|
+
resolveResponseRefs(document, operation);
|
|
962
|
+
const responseBody = getResponseBody(getResponseByStatusCode({
|
|
963
|
+
document,
|
|
964
|
+
operation,
|
|
965
|
+
statusCode
|
|
966
|
+
}), options.contentType);
|
|
910
967
|
if (responseBody === false) return {};
|
|
911
968
|
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
|
|
912
969
|
if (!schema) return {};
|
|
@@ -922,16 +979,25 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
|
922
979
|
*/
|
|
923
980
|
function getRequestSchema(document, operation, options = {}) {
|
|
924
981
|
if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
|
|
925
|
-
const requestBody =
|
|
982
|
+
const requestBody = getRequestContent({
|
|
983
|
+
document,
|
|
984
|
+
operation,
|
|
985
|
+
mediaType: options.contentType
|
|
986
|
+
});
|
|
926
987
|
if (requestBody === false) return null;
|
|
988
|
+
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
927
989
|
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
990
|
+
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
991
|
+
type: "string",
|
|
992
|
+
contentMediaType: "application/octet-stream"
|
|
993
|
+
};
|
|
928
994
|
if (!schema) return null;
|
|
929
995
|
return dereferenceWithRef(document, schema);
|
|
930
996
|
}
|
|
931
997
|
/**
|
|
932
998
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
933
999
|
*
|
|
934
|
-
* Only flattens when every member is a plain fragment
|
|
1000
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
935
1001
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
936
1002
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
937
1003
|
*
|
|
@@ -939,9 +1005,12 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
939
1005
|
* ```ts
|
|
940
1006
|
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
941
1007
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
1008
|
+
* ```
|
|
942
1009
|
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```ts
|
|
943
1012
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
944
|
-
* // returned unchanged
|
|
1013
|
+
* // returned unchanged, contains a $ref
|
|
945
1014
|
* ```
|
|
946
1015
|
*/
|
|
947
1016
|
/**
|
|
@@ -957,17 +1026,17 @@ function hasStructuralKeywords(fragment) {
|
|
|
957
1026
|
function flattenSchema(schema) {
|
|
958
1027
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
|
|
959
1028
|
const allOfFragments = schema.allOf;
|
|
960
|
-
if (allOfFragments.some((item) => (
|
|
1029
|
+
if (allOfFragments.some((item) => isReference(item))) return schema;
|
|
961
1030
|
if (allOfFragments.some(hasStructuralKeywords)) return schema;
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment))
|
|
1031
|
+
const { allOf: _allOf, ...rest } = schema;
|
|
1032
|
+
const merged = rest;
|
|
1033
|
+
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
965
1034
|
return merged;
|
|
966
1035
|
}
|
|
967
1036
|
/**
|
|
968
1037
|
* Extracts the inline schema from a media-type `content` map.
|
|
969
1038
|
*
|
|
970
|
-
* Prefers `preferredContentType` when given
|
|
1039
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
971
1040
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
972
1041
|
*
|
|
973
1042
|
* @example
|
|
@@ -986,21 +1055,22 @@ function extractSchemaFromContent(content, preferredContentType) {
|
|
|
986
1055
|
/**
|
|
987
1056
|
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
988
1057
|
*/
|
|
989
|
-
function collectRefs(schema
|
|
1058
|
+
function* collectRefs(schema) {
|
|
990
1059
|
if (Array.isArray(schema)) {
|
|
991
|
-
for (const item of schema) collectRefs(item
|
|
992
|
-
return
|
|
1060
|
+
for (const item of schema) yield* collectRefs(item);
|
|
1061
|
+
return;
|
|
993
1062
|
}
|
|
994
1063
|
if (schema && typeof schema === "object") for (const key in schema) {
|
|
995
1064
|
const value = schema[key];
|
|
996
|
-
if (key === "$ref" && typeof value === "string") {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1065
|
+
if (!(key === "$ref" && typeof value === "string")) {
|
|
1066
|
+
yield* collectRefs(value);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
1070
|
+
const name = value.slice(21);
|
|
1071
|
+
if (name) yield name;
|
|
1072
|
+
}
|
|
1002
1073
|
}
|
|
1003
|
-
return refs;
|
|
1004
1074
|
}
|
|
1005
1075
|
/**
|
|
1006
1076
|
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
@@ -1016,7 +1086,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
|
|
|
1016
1086
|
*/
|
|
1017
1087
|
function sortSchemas(schemas) {
|
|
1018
1088
|
const deps = /* @__PURE__ */ new Map();
|
|
1019
|
-
for (const [name, schema] of Object.entries(schemas)) deps.set(name,
|
|
1089
|
+
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
1020
1090
|
const sorted = [];
|
|
1021
1091
|
const visited = /* @__PURE__ */ new Set();
|
|
1022
1092
|
function visit(name, stack) {
|
|
@@ -1037,9 +1107,6 @@ const semanticSuffixes = {
|
|
|
1037
1107
|
responses: "Response",
|
|
1038
1108
|
requestBodies: "Request"
|
|
1039
1109
|
};
|
|
1040
|
-
function getSemanticSuffix(source) {
|
|
1041
|
-
return semanticSuffixes[source];
|
|
1042
|
-
}
|
|
1043
1110
|
function resolveSchemaRef(document, schema) {
|
|
1044
1111
|
if (!isReference(schema)) return schema;
|
|
1045
1112
|
const resolved = resolveRef(document, schema.$ref);
|
|
@@ -1088,13 +1155,13 @@ function getSchemas(document, { contentType }) {
|
|
|
1088
1155
|
let hasMultipleSources = false;
|
|
1089
1156
|
if (!isSingle) {
|
|
1090
1157
|
const firstSource = items[0].source;
|
|
1091
|
-
for (
|
|
1158
|
+
for (const item of items) if (item.source !== firstSource) {
|
|
1092
1159
|
hasMultipleSources = true;
|
|
1093
1160
|
break;
|
|
1094
1161
|
}
|
|
1095
1162
|
}
|
|
1096
1163
|
items.forEach((item, index) => {
|
|
1097
|
-
const suffix = isSingle ? "" : hasMultipleSources ?
|
|
1164
|
+
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
|
|
1098
1165
|
const uniqueName = item.originalName + suffix;
|
|
1099
1166
|
schemas[uniqueName] = item.schema;
|
|
1100
1167
|
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
@@ -1107,7 +1174,7 @@ function getSchemas(document, { contentType }) {
|
|
|
1107
1174
|
}
|
|
1108
1175
|
/**
|
|
1109
1176
|
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
1110
|
-
* Returns `null` when `dateType: false`,
|
|
1177
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
1111
1178
|
*/
|
|
1112
1179
|
function getDateType(options, format) {
|
|
1113
1180
|
if (!options.dateType) return null;
|
|
@@ -1141,6 +1208,15 @@ function getDateType(options, format) {
|
|
|
1141
1208
|
/**
|
|
1142
1209
|
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
1143
1210
|
*/
|
|
1211
|
+
/**
|
|
1212
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1213
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1214
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1215
|
+
*/
|
|
1216
|
+
function extractExamples(schema) {
|
|
1217
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1218
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1219
|
+
}
|
|
1144
1220
|
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
1145
1221
|
return {
|
|
1146
1222
|
name,
|
|
@@ -1151,15 +1227,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1151
1227
|
readOnly: schema.readOnly,
|
|
1152
1228
|
writeOnly: schema.writeOnly,
|
|
1153
1229
|
default: defaultValue,
|
|
1154
|
-
|
|
1230
|
+
examples: extractExamples(schema),
|
|
1231
|
+
format: schema.format
|
|
1155
1232
|
};
|
|
1156
1233
|
}
|
|
1157
1234
|
/**
|
|
1158
1235
|
* Returns all request body content type keys for an operation.
|
|
1159
1236
|
*
|
|
1160
|
-
* The requestBody is dereferenced
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1237
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1238
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1239
|
+
* available content types even for referenced bodies.
|
|
1163
1240
|
*
|
|
1164
1241
|
* @example
|
|
1165
1242
|
* ```ts
|
|
@@ -1173,15 +1250,38 @@ function getRequestBodyContentTypes(document, operation) {
|
|
|
1173
1250
|
if (!body) return [];
|
|
1174
1251
|
return body.content ? Object.keys(body.content) : [];
|
|
1175
1252
|
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Returns all response content type keys for an operation at a given status code.
|
|
1255
|
+
*
|
|
1256
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1257
|
+
* so the returned list reflects the available content types even for referenced responses.
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* ```ts
|
|
1261
|
+
* getResponseBodyContentTypes(document, operation, 200)
|
|
1262
|
+
* // ['application/json', 'application/xml']
|
|
1263
|
+
* ```
|
|
1264
|
+
*/
|
|
1265
|
+
function getResponseBodyContentTypes(document, operation, statusCode) {
|
|
1266
|
+
resolveResponseRefs(document, operation);
|
|
1267
|
+
const responseObj = getResponseByStatusCode({
|
|
1268
|
+
document,
|
|
1269
|
+
operation,
|
|
1270
|
+
statusCode
|
|
1271
|
+
});
|
|
1272
|
+
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1273
|
+
const body = responseObj;
|
|
1274
|
+
return body.content ? Object.keys(body.content) : [];
|
|
1275
|
+
}
|
|
1176
1276
|
//#endregion
|
|
1177
1277
|
//#region src/parser.ts
|
|
1178
1278
|
/**
|
|
1179
1279
|
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1180
1280
|
*
|
|
1181
1281
|
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1182
|
-
* from the array to its items sub-schema,
|
|
1282
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1183
1283
|
*
|
|
1184
|
-
* @note
|
|
1284
|
+
* @note A defensive measure for non-compliant specs.
|
|
1185
1285
|
*/
|
|
1186
1286
|
function normalizeArrayEnum(schema) {
|
|
1187
1287
|
const normalizedItems = {
|
|
@@ -1195,15 +1295,48 @@ function normalizeArrayEnum(schema) {
|
|
|
1195
1295
|
};
|
|
1196
1296
|
}
|
|
1197
1297
|
/**
|
|
1298
|
+
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
1299
|
+
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
1300
|
+
*/
|
|
1301
|
+
function createNullSchema(schema, name, nullable) {
|
|
1302
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1303
|
+
type: "null",
|
|
1304
|
+
primitive: "null",
|
|
1305
|
+
name,
|
|
1306
|
+
title: schema.title,
|
|
1307
|
+
description: schema.description,
|
|
1308
|
+
deprecated: schema.deprecated,
|
|
1309
|
+
nullable,
|
|
1310
|
+
format: schema.format
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Names the inline enums on a property's schema, and on each item when the property is a tuple, from
|
|
1315
|
+
* the parent and property name. Wraps `macroEnumName` at the property construction site.
|
|
1316
|
+
*/
|
|
1317
|
+
function nameEnums(node, options) {
|
|
1318
|
+
const macro = (0, _kubb_ast.macroEnumName)(options);
|
|
1319
|
+
const named = _kubb_ast.ast.applyMacros(node, [macro], { depth: "shallow" });
|
|
1320
|
+
const tupleNode = _kubb_ast.ast.narrowSchema(named, "tuple");
|
|
1321
|
+
if (tupleNode?.items) {
|
|
1322
|
+
const namedItems = tupleNode.items.map((item) => _kubb_ast.ast.applyMacros(item, [macro], { depth: "shallow" }));
|
|
1323
|
+
if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
|
|
1324
|
+
...tupleNode,
|
|
1325
|
+
items: namedItems
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
return named;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1198
1331
|
* Factory function that creates schema and operation converters for a given OpenAPI context.
|
|
1199
1332
|
*
|
|
1200
1333
|
* Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
|
|
1201
1334
|
* Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
|
|
1202
|
-
*
|
|
1335
|
+
* which works because function declarations hoist.
|
|
1203
1336
|
*
|
|
1204
|
-
* @
|
|
1337
|
+
* @internal
|
|
1205
1338
|
*/
|
|
1206
|
-
function createSchemaParser(ctx) {
|
|
1339
|
+
function createSchemaParser(ctx, dialect = oasDialect) {
|
|
1207
1340
|
const document = ctx.document;
|
|
1208
1341
|
/**
|
|
1209
1342
|
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
@@ -1211,6 +1344,33 @@ function createSchemaParser(ctx) {
|
|
|
1211
1344
|
*/
|
|
1212
1345
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1213
1346
|
/**
|
|
1347
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
1348
|
+
*
|
|
1349
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
1350
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
1351
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1352
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1353
|
+
*/
|
|
1354
|
+
const resolvedRefCache = /* @__PURE__ */ new Map();
|
|
1355
|
+
/**
|
|
1356
|
+
* Memoized record of whether a `$ref` path resolves to a node the document actually defines.
|
|
1357
|
+
* A circular ref still resolves to an existing target, so this stays `true` for cycles and only
|
|
1358
|
+
* goes `false` for a `$ref` that points at a component the spec never declares.
|
|
1359
|
+
*/
|
|
1360
|
+
const refExistence = /* @__PURE__ */ new Map();
|
|
1361
|
+
function refExists(refPath) {
|
|
1362
|
+
if (!refExistence.has(refPath)) {
|
|
1363
|
+
let exists = false;
|
|
1364
|
+
try {
|
|
1365
|
+
exists = !!dialect.schema.resolveRef(document, refPath);
|
|
1366
|
+
} catch {
|
|
1367
|
+
exists = false;
|
|
1368
|
+
}
|
|
1369
|
+
refExistence.set(refPath, exists);
|
|
1370
|
+
}
|
|
1371
|
+
return refExistence.get(refPath) ?? false;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1214
1374
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1215
1375
|
*
|
|
1216
1376
|
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
@@ -1219,20 +1379,30 @@ function createSchemaParser(ctx) {
|
|
|
1219
1379
|
* Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
|
|
1220
1380
|
*/
|
|
1221
1381
|
function convertRef({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1222
|
-
let resolvedSchema;
|
|
1382
|
+
let resolvedSchema = null;
|
|
1223
1383
|
const refPath = schema.$ref;
|
|
1224
|
-
if (refPath && !resolvingRefs.has(refPath))
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1384
|
+
if (refPath && !resolvingRefs.has(refPath)) {
|
|
1385
|
+
if (!resolvedRefCache.has(refPath)) {
|
|
1386
|
+
try {
|
|
1387
|
+
const referenced = dialect.schema.resolveRef(document, refPath);
|
|
1388
|
+
if (referenced) {
|
|
1389
|
+
resolvingRefs.add(refPath);
|
|
1390
|
+
resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
|
|
1391
|
+
resolvingRefs.delete(refPath);
|
|
1392
|
+
}
|
|
1393
|
+
} catch {}
|
|
1394
|
+
resolvedRefCache.set(refPath, resolvedSchema);
|
|
1230
1395
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1396
|
+
resolvedSchema = resolvedRefCache.get(refPath) ?? null;
|
|
1397
|
+
}
|
|
1398
|
+
if (refPath && document.components && !refExists(refPath)) return _kubb_ast.ast.factory.createSchema({
|
|
1399
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1400
|
+
type: "unknown"
|
|
1401
|
+
});
|
|
1402
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1233
1403
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1234
1404
|
type: "ref",
|
|
1235
|
-
name:
|
|
1405
|
+
name: (0, _kubb_ast.extractRefName)(schema.$ref),
|
|
1236
1406
|
ref: schema.$ref,
|
|
1237
1407
|
schema: resolvedSchema
|
|
1238
1408
|
});
|
|
@@ -1245,12 +1415,12 @@ function createSchemaParser(ctx) {
|
|
|
1245
1415
|
const [memberSchema] = schema.allOf;
|
|
1246
1416
|
const memberNode = parseSchema({
|
|
1247
1417
|
schema: memberSchema,
|
|
1248
|
-
name
|
|
1418
|
+
name
|
|
1249
1419
|
}, rawOptions);
|
|
1250
1420
|
const { kind: _kind, ...memberNodeProps } = memberNode;
|
|
1251
1421
|
const mergedNullable = nullable || memberNode.nullable || void 0;
|
|
1252
1422
|
const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
|
|
1253
|
-
return
|
|
1423
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1254
1424
|
...memberNodeProps,
|
|
1255
1425
|
name,
|
|
1256
1426
|
title: schema.title ?? memberNode.title,
|
|
@@ -1260,22 +1430,23 @@ function createSchemaParser(ctx) {
|
|
|
1260
1430
|
readOnly: schema.readOnly ?? memberNode.readOnly,
|
|
1261
1431
|
writeOnly: schema.writeOnly ?? memberNode.writeOnly,
|
|
1262
1432
|
default: mergedDefault,
|
|
1263
|
-
|
|
1264
|
-
pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
|
|
1433
|
+
examples: extractExamples(schema) ?? memberNode.examples,
|
|
1434
|
+
pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
|
|
1435
|
+
format: schema.format ?? memberNode.format
|
|
1265
1436
|
});
|
|
1266
1437
|
}
|
|
1267
1438
|
const filteredDiscriminantValues = [];
|
|
1268
1439
|
const allOfMembers = schema.allOf.filter((item) => {
|
|
1269
|
-
if (!isReference(item) || !name) return true;
|
|
1270
|
-
const deref = resolveRef(document, item.$ref);
|
|
1271
|
-
if (!deref || !isDiscriminator(deref)) return true;
|
|
1440
|
+
if (!dialect.schema.isReference(item) || !name) return true;
|
|
1441
|
+
const deref = dialect.schema.resolveRef(document, item.$ref);
|
|
1442
|
+
if (!deref || !dialect.schema.isDiscriminator(deref)) return true;
|
|
1272
1443
|
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1273
1444
|
if (!parentUnion) return true;
|
|
1274
1445
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1275
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1446
|
+
const inOneOf = parentUnion.some((oneOfItem) => dialect.schema.isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1276
1447
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1277
1448
|
if (inOneOf || inMapping) {
|
|
1278
|
-
const discriminatorValue =
|
|
1449
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1279
1450
|
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1280
1451
|
propertyName: deref.discriminator.propertyName,
|
|
1281
1452
|
value: discriminatorValue
|
|
@@ -1283,22 +1454,28 @@ function createSchemaParser(ctx) {
|
|
|
1283
1454
|
return false;
|
|
1284
1455
|
}
|
|
1285
1456
|
return true;
|
|
1286
|
-
}).map((s) => parseSchema({
|
|
1457
|
+
}).map((s) => parseSchema({
|
|
1458
|
+
schema: s,
|
|
1459
|
+
name
|
|
1460
|
+
}, rawOptions));
|
|
1287
1461
|
const syntheticStart = allOfMembers.length;
|
|
1288
1462
|
if (Array.isArray(schema.required) && schema.required.length) {
|
|
1289
1463
|
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
|
|
1290
1464
|
const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
|
|
1291
1465
|
if (missingRequired.length) {
|
|
1292
1466
|
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1293
|
-
if (!isReference(item)) return [item];
|
|
1294
|
-
const deref = resolveRef(document, item.$ref);
|
|
1295
|
-
return deref && !isReference(deref) ? [deref] : [];
|
|
1467
|
+
if (!dialect.schema.isReference(item)) return [item];
|
|
1468
|
+
const deref = dialect.schema.resolveRef(document, item.$ref);
|
|
1469
|
+
return deref && !dialect.schema.isReference(deref) ? [deref] : [];
|
|
1296
1470
|
});
|
|
1297
1471
|
for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
|
|
1298
|
-
allOfMembers.push(parseSchema({
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1472
|
+
allOfMembers.push(parseSchema({
|
|
1473
|
+
schema: {
|
|
1474
|
+
properties: { [key]: resolved.properties[key] },
|
|
1475
|
+
required: [key]
|
|
1476
|
+
},
|
|
1477
|
+
name
|
|
1478
|
+
}, rawOptions));
|
|
1302
1479
|
break;
|
|
1303
1480
|
}
|
|
1304
1481
|
}
|
|
@@ -1307,13 +1484,13 @@ function createSchemaParser(ctx) {
|
|
|
1307
1484
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1308
1485
|
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1309
1486
|
}
|
|
1310
|
-
for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(
|
|
1487
|
+
for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1311
1488
|
propertyName,
|
|
1312
1489
|
value
|
|
1313
1490
|
}));
|
|
1314
|
-
return
|
|
1491
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1315
1492
|
type: "intersection",
|
|
1316
|
-
members: [...
|
|
1493
|
+
members: [...(0, _kubb_ast.mergeAdjacentObjectsLazy)(allOfMembers.slice(0, syntheticStart)), ...(0, _kubb_ast.mergeAdjacentObjectsLazy)(allOfMembers.slice(syntheticStart))],
|
|
1317
1494
|
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1318
1495
|
});
|
|
1319
1496
|
}
|
|
@@ -1322,81 +1499,106 @@ function createSchemaParser(ctx) {
|
|
|
1322
1499
|
*/
|
|
1323
1500
|
function convertUnion({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1324
1501
|
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1325
|
-
const discriminatorProperty =
|
|
1502
|
+
const discriminatorProperty = _kubb_ast.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1326
1503
|
if (!discriminatorProperty) return null;
|
|
1327
|
-
return
|
|
1504
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1328
1505
|
type: "object",
|
|
1329
1506
|
primitive: "object",
|
|
1330
1507
|
properties: [discriminatorProperty]
|
|
1331
1508
|
});
|
|
1332
1509
|
}
|
|
1510
|
+
function resolveRefSilent($ref) {
|
|
1511
|
+
if (!$ref.startsWith("#")) return null;
|
|
1512
|
+
return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
|
|
1513
|
+
}
|
|
1514
|
+
function implicitDiscriminantValue(member) {
|
|
1515
|
+
if (!discriminator || discriminator.mapping || !dialect.schema.isReference(member)) return null;
|
|
1516
|
+
const value = (0, _kubb_ast.extractRefName)(member.$ref);
|
|
1517
|
+
if (!value) return null;
|
|
1518
|
+
const variant = resolveRefSilent(member.$ref);
|
|
1519
|
+
if (!variant) return null;
|
|
1520
|
+
const propertyName = discriminator.propertyName;
|
|
1521
|
+
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1522
|
+
function constrains(v) {
|
|
1523
|
+
const prop = v.properties?.[propertyName];
|
|
1524
|
+
const resolved = prop && dialect.schema.isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
|
|
1525
|
+
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1526
|
+
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1527
|
+
if (!composition) return false;
|
|
1528
|
+
return composition.some((m) => {
|
|
1529
|
+
if (!dialect.schema.isReference(m)) return constrains(m);
|
|
1530
|
+
if (seen.has(m.$ref)) return false;
|
|
1531
|
+
seen.add(m.$ref);
|
|
1532
|
+
const r = resolveRefSilent(m.$ref);
|
|
1533
|
+
return r ? constrains(r) : false;
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
return constrains(variant) ? null : value;
|
|
1537
|
+
}
|
|
1333
1538
|
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1334
1539
|
const strategy = schema.oneOf ? "one" : "any";
|
|
1335
1540
|
const unionBase = {
|
|
1336
1541
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1337
|
-
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1542
|
+
discriminatorPropertyName: dialect.schema.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1338
1543
|
strategy
|
|
1339
1544
|
};
|
|
1340
|
-
const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
})() : void 0;
|
|
1348
|
-
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
1545
|
+
const discriminator = dialect.schema.isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
1546
|
+
const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
|
|
1547
|
+
const sharedPropertiesNode = schema.properties ? parseSchema({
|
|
1548
|
+
schema: memberBaseSchema,
|
|
1549
|
+
name
|
|
1550
|
+
}, rawOptions) : void 0;
|
|
1551
|
+
if (sharedPropertiesNode || discriminator) {
|
|
1349
1552
|
const members = unionMembers.map((s) => {
|
|
1350
|
-
const ref = isReference(s) ? s.$ref : void 0;
|
|
1351
|
-
const discriminatorValue =
|
|
1352
|
-
const memberNode = parseSchema({
|
|
1553
|
+
const ref = dialect.schema.isReference(s) ? s.$ref : void 0;
|
|
1554
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
|
|
1555
|
+
const memberNode = parseSchema({
|
|
1556
|
+
schema: s,
|
|
1557
|
+
name
|
|
1558
|
+
}, rawOptions);
|
|
1353
1559
|
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1354
|
-
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(
|
|
1355
|
-
node: sharedPropertiesNode,
|
|
1560
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(_kubb_ast.ast.applyMacros(sharedPropertiesNode, [(0, _kubb_ast.macroDiscriminatorEnum)({
|
|
1356
1561
|
propertyName: discriminator.propertyName,
|
|
1357
1562
|
values: [discriminatorValue]
|
|
1358
|
-
}), discriminator.propertyName) : void 0;
|
|
1359
|
-
return
|
|
1563
|
+
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1564
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1360
1565
|
type: "intersection",
|
|
1361
|
-
members: [memberNode, narrowedDiscriminatorNode ??
|
|
1566
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1362
1567
|
propertyName: discriminator.propertyName,
|
|
1363
1568
|
value: discriminatorValue
|
|
1364
1569
|
})]
|
|
1365
1570
|
});
|
|
1366
1571
|
});
|
|
1367
|
-
const unionNode =
|
|
1572
|
+
const unionNode = _kubb_ast.ast.factory.createSchema({
|
|
1368
1573
|
type: "union",
|
|
1369
1574
|
...unionBase,
|
|
1370
1575
|
members
|
|
1371
1576
|
});
|
|
1372
1577
|
if (!sharedPropertiesNode) return unionNode;
|
|
1373
|
-
return
|
|
1578
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1374
1579
|
type: "intersection",
|
|
1375
1580
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1376
1581
|
members: [unionNode, sharedPropertiesNode]
|
|
1377
1582
|
});
|
|
1378
1583
|
}
|
|
1379
|
-
|
|
1584
|
+
const unionNode = _kubb_ast.ast.factory.createSchema({
|
|
1380
1585
|
type: "union",
|
|
1381
1586
|
...unionBase,
|
|
1382
|
-
members:
|
|
1587
|
+
members: unionMembers.map((s) => parseSchema({
|
|
1588
|
+
schema: s,
|
|
1589
|
+
name
|
|
1590
|
+
}, rawOptions))
|
|
1383
1591
|
});
|
|
1592
|
+
return _kubb_ast.ast.applyMacros(unionNode, [_kubb_ast.macroSimplifyUnion], { depth: "shallow" });
|
|
1384
1593
|
}
|
|
1385
1594
|
/**
|
|
1386
1595
|
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1387
1596
|
*/
|
|
1388
1597
|
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1389
1598
|
const constValue = schema.const;
|
|
1390
|
-
if (constValue === null) return
|
|
1391
|
-
type: "null",
|
|
1392
|
-
primitive: "null",
|
|
1393
|
-
name,
|
|
1394
|
-
title: schema.title,
|
|
1395
|
-
description: schema.description,
|
|
1396
|
-
deprecated: schema.deprecated
|
|
1397
|
-
});
|
|
1599
|
+
if (constValue === null) return createNullSchema(schema, name);
|
|
1398
1600
|
const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
|
|
1399
|
-
return
|
|
1601
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1400
1602
|
type: "enum",
|
|
1401
1603
|
primitive: constPrimitive,
|
|
1402
1604
|
enumValues: [constValue],
|
|
@@ -1409,7 +1611,7 @@ function createSchemaParser(ctx) {
|
|
|
1409
1611
|
*/
|
|
1410
1612
|
function convertFormat({ schema, name, nullable, defaultValue, options }) {
|
|
1411
1613
|
const base = buildSchemaNode(schema, name, nullable, defaultValue);
|
|
1412
|
-
if (schema.format === "int64") return
|
|
1614
|
+
if (schema.format === "int64") return _kubb_ast.ast.factory.createSchema({
|
|
1413
1615
|
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1414
1616
|
primitive: "integer",
|
|
1415
1617
|
...base,
|
|
@@ -1421,14 +1623,14 @@ function createSchemaParser(ctx) {
|
|
|
1421
1623
|
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1422
1624
|
const dateType = getDateType(options, schema.format);
|
|
1423
1625
|
if (!dateType) return null;
|
|
1424
|
-
if (dateType.type === "datetime") return
|
|
1626
|
+
if (dateType.type === "datetime") return _kubb_ast.ast.factory.createSchema({
|
|
1425
1627
|
...base,
|
|
1426
1628
|
primitive: "string",
|
|
1427
1629
|
type: "datetime",
|
|
1428
1630
|
offset: dateType.offset,
|
|
1429
1631
|
local: dateType.local
|
|
1430
1632
|
});
|
|
1431
|
-
return
|
|
1633
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1432
1634
|
...base,
|
|
1433
1635
|
primitive: "string",
|
|
1434
1636
|
type: dateType.type,
|
|
@@ -1438,36 +1640,36 @@ function createSchemaParser(ctx) {
|
|
|
1438
1640
|
const specialType = getSchemaType(schema.format);
|
|
1439
1641
|
if (!specialType) return null;
|
|
1440
1642
|
const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
|
|
1441
|
-
if (specialType === "number" || specialType === "integer" || specialType === "bigint") return
|
|
1643
|
+
if (specialType === "number" || specialType === "integer" || specialType === "bigint") return _kubb_ast.ast.factory.createSchema({
|
|
1442
1644
|
...base,
|
|
1443
1645
|
primitive: specialPrimitive,
|
|
1444
1646
|
type: specialType
|
|
1445
1647
|
});
|
|
1446
|
-
if (specialType === "url") return
|
|
1648
|
+
if (specialType === "url") return _kubb_ast.ast.factory.createSchema({
|
|
1447
1649
|
...base,
|
|
1448
1650
|
primitive: "string",
|
|
1449
1651
|
type: "url",
|
|
1450
1652
|
min: schema.minLength,
|
|
1451
1653
|
max: schema.maxLength
|
|
1452
1654
|
});
|
|
1453
|
-
if (specialType === "ipv4") return
|
|
1655
|
+
if (specialType === "ipv4") return _kubb_ast.ast.factory.createSchema({
|
|
1454
1656
|
...base,
|
|
1455
1657
|
primitive: "string",
|
|
1456
1658
|
type: "ipv4"
|
|
1457
1659
|
});
|
|
1458
|
-
if (specialType === "ipv6") return
|
|
1660
|
+
if (specialType === "ipv6") return _kubb_ast.ast.factory.createSchema({
|
|
1459
1661
|
...base,
|
|
1460
1662
|
primitive: "string",
|
|
1461
1663
|
type: "ipv6"
|
|
1462
1664
|
});
|
|
1463
|
-
if (specialType === "uuid" || specialType === "email") return
|
|
1665
|
+
if (specialType === "uuid" || specialType === "email") return _kubb_ast.ast.factory.createSchema({
|
|
1464
1666
|
...base,
|
|
1465
1667
|
primitive: "string",
|
|
1466
1668
|
type: specialType,
|
|
1467
1669
|
min: schema.minLength,
|
|
1468
1670
|
max: schema.maxLength
|
|
1469
1671
|
});
|
|
1470
|
-
return
|
|
1672
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1471
1673
|
...base,
|
|
1472
1674
|
primitive: specialPrimitive,
|
|
1473
1675
|
type: specialType
|
|
@@ -1483,6 +1685,7 @@ function createSchemaParser(ctx) {
|
|
|
1483
1685
|
}, rawOptions);
|
|
1484
1686
|
const nullInEnum = schema.enum.includes(null);
|
|
1485
1687
|
const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
|
|
1688
|
+
if (nullInEnum && filteredValues.length === 0) return createNullSchema(schema, name);
|
|
1486
1689
|
const enumNullable = nullable || nullInEnum || void 0;
|
|
1487
1690
|
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
1488
1691
|
const enumPrimitive = getPrimitiveType(type);
|
|
@@ -1497,21 +1700,25 @@ function createSchemaParser(ctx) {
|
|
|
1497
1700
|
readOnly: schema.readOnly,
|
|
1498
1701
|
writeOnly: schema.writeOnly,
|
|
1499
1702
|
default: enumDefault,
|
|
1500
|
-
|
|
1703
|
+
examples: extractExamples(schema),
|
|
1704
|
+
format: schema.format
|
|
1501
1705
|
};
|
|
1502
1706
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1503
|
-
|
|
1707
|
+
const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
|
|
1708
|
+
if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1504
1709
|
const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
|
|
1505
1710
|
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1711
|
+
const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
|
|
1506
1712
|
const uniqueValues = [...new Set(filteredValues)];
|
|
1507
1713
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1508
|
-
return
|
|
1714
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1509
1715
|
...enumBase,
|
|
1510
1716
|
primitive: enumPrimitiveType,
|
|
1511
1717
|
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1512
1718
|
name: String(rawEnumNames?.[index] ?? value),
|
|
1513
1719
|
value,
|
|
1514
|
-
primitive: enumPrimitiveType
|
|
1720
|
+
primitive: enumPrimitiveType,
|
|
1721
|
+
description: rawEnumDescriptions?.[index]
|
|
1515
1722
|
})).filter((entry) => {
|
|
1516
1723
|
if (seenNames.has(entry.name)) return false;
|
|
1517
1724
|
seenNames.add(entry.name);
|
|
@@ -1519,7 +1726,7 @@ function createSchemaParser(ctx) {
|
|
|
1519
1726
|
})
|
|
1520
1727
|
});
|
|
1521
1728
|
}
|
|
1522
|
-
return
|
|
1729
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1523
1730
|
...enumBase,
|
|
1524
1731
|
enumValues: [...new Set(filteredValues)]
|
|
1525
1732
|
});
|
|
@@ -1531,21 +1738,16 @@ function createSchemaParser(ctx) {
|
|
|
1531
1738
|
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
1532
1739
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
1533
1740
|
const resolvedPropSchema = propSchema;
|
|
1534
|
-
const propNullable = isNullable(resolvedPropSchema);
|
|
1535
|
-
const
|
|
1741
|
+
const propNullable = dialect.schema.isNullable(resolvedPropSchema);
|
|
1742
|
+
const schemaNode = nameEnums(parseSchema({
|
|
1536
1743
|
schema: resolvedPropSchema,
|
|
1537
|
-
name:
|
|
1538
|
-
}, rawOptions)
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
...tupleNode,
|
|
1545
|
-
items: namedItems
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
return _kubb_core.ast.createProperty({
|
|
1744
|
+
name: (0, _kubb_ast.childName)(name, propName)
|
|
1745
|
+
}, rawOptions), {
|
|
1746
|
+
parentName: name,
|
|
1747
|
+
propName,
|
|
1748
|
+
enumSuffix: options.enumSuffix
|
|
1749
|
+
});
|
|
1750
|
+
return _kubb_ast.ast.factory.createProperty({
|
|
1549
1751
|
name: propName,
|
|
1550
1752
|
schema: {
|
|
1551
1753
|
...schemaNode,
|
|
@@ -1555,14 +1757,15 @@ function createSchemaParser(ctx) {
|
|
|
1555
1757
|
});
|
|
1556
1758
|
}) : [];
|
|
1557
1759
|
const additionalProperties = schema.additionalProperties;
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1760
|
+
const additionalPropertiesNode = (() => {
|
|
1761
|
+
if (additionalProperties === true) return true;
|
|
1762
|
+
if (additionalProperties === false) return false;
|
|
1763
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) return parseSchema({ schema: additionalProperties }, rawOptions);
|
|
1764
|
+
if (additionalProperties) return _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
|
|
1765
|
+
})();
|
|
1563
1766
|
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1564
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ?
|
|
1565
|
-
const objectNode =
|
|
1767
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? _kubb_ast.ast.factory.createSchema({ type: options.unknownType }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
|
|
1768
|
+
const objectNode = _kubb_ast.ast.factory.createSchema({
|
|
1566
1769
|
type: "object",
|
|
1567
1770
|
primitive: "object",
|
|
1568
1771
|
properties,
|
|
@@ -1572,16 +1775,15 @@ function createSchemaParser(ctx) {
|
|
|
1572
1775
|
maxProperties: schema.maxProperties,
|
|
1573
1776
|
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1574
1777
|
});
|
|
1575
|
-
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1778
|
+
if (dialect.schema.isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1576
1779
|
const discPropName = schema.discriminator.propertyName;
|
|
1577
1780
|
const values = Object.keys(schema.discriminator.mapping);
|
|
1578
|
-
const enumName = name ?
|
|
1579
|
-
return
|
|
1580
|
-
node: objectNode,
|
|
1781
|
+
const enumName = name ? (0, _kubb_ast.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
|
|
1782
|
+
return _kubb_ast.ast.applyMacros(objectNode, [(0, _kubb_ast.macroDiscriminatorEnum)({
|
|
1581
1783
|
propertyName: discPropName,
|
|
1582
1784
|
values,
|
|
1583
1785
|
enumName
|
|
1584
|
-
});
|
|
1786
|
+
})], { depth: "shallow" });
|
|
1585
1787
|
}
|
|
1586
1788
|
return objectNode;
|
|
1587
1789
|
}
|
|
@@ -1590,8 +1792,8 @@ function createSchemaParser(ctx) {
|
|
|
1590
1792
|
*/
|
|
1591
1793
|
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1592
1794
|
const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
|
|
1593
|
-
const rest = schema.items ?
|
|
1594
|
-
return
|
|
1795
|
+
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? _kubb_ast.ast.factory.createSchema({ type: "any" }) : parseSchema({ schema: schema.items }, rawOptions);
|
|
1796
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1595
1797
|
type: "tuple",
|
|
1596
1798
|
primitive: "array",
|
|
1597
1799
|
items: tupleItems,
|
|
@@ -1606,12 +1808,12 @@ function createSchemaParser(ctx) {
|
|
|
1606
1808
|
*/
|
|
1607
1809
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
|
|
1608
1810
|
const rawItems = schema.items;
|
|
1609
|
-
const itemName = rawItems?.enum?.length && name ?
|
|
1811
|
+
const itemName = rawItems?.enum?.length && name ? (0, _kubb_ast.enumPropName)(null, name, options.enumSuffix) : name;
|
|
1610
1812
|
const items = rawItems ? [parseSchema({
|
|
1611
1813
|
schema: rawItems,
|
|
1612
1814
|
name: itemName
|
|
1613
1815
|
}, rawOptions)] : [];
|
|
1614
|
-
return
|
|
1816
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1615
1817
|
type: "array",
|
|
1616
1818
|
primitive: "array",
|
|
1617
1819
|
items,
|
|
@@ -1625,7 +1827,7 @@ function createSchemaParser(ctx) {
|
|
|
1625
1827
|
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1626
1828
|
*/
|
|
1627
1829
|
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1628
|
-
return
|
|
1830
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1629
1831
|
type: "string",
|
|
1630
1832
|
primitive: "string",
|
|
1631
1833
|
min: schema.minLength,
|
|
@@ -1638,7 +1840,7 @@ function createSchemaParser(ctx) {
|
|
|
1638
1840
|
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1639
1841
|
*/
|
|
1640
1842
|
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1641
|
-
return
|
|
1843
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1642
1844
|
type,
|
|
1643
1845
|
primitive: type,
|
|
1644
1846
|
min: schema.minimum,
|
|
@@ -1653,32 +1855,132 @@ function createSchemaParser(ctx) {
|
|
|
1653
1855
|
* Converts a `type: 'boolean'` schema.
|
|
1654
1856
|
*/
|
|
1655
1857
|
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1656
|
-
return
|
|
1858
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1657
1859
|
type: "boolean",
|
|
1658
1860
|
primitive: "boolean",
|
|
1659
1861
|
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1660
1862
|
});
|
|
1661
1863
|
}
|
|
1662
1864
|
/**
|
|
1663
|
-
* Converts
|
|
1865
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
1866
|
+
* into a `blob` node.
|
|
1664
1867
|
*/
|
|
1665
|
-
function
|
|
1666
|
-
return
|
|
1667
|
-
type: "
|
|
1668
|
-
primitive: "
|
|
1669
|
-
name,
|
|
1670
|
-
title: schema.title,
|
|
1671
|
-
description: schema.description,
|
|
1672
|
-
deprecated: schema.deprecated,
|
|
1673
|
-
nullable
|
|
1868
|
+
function convertBlob({ schema, name, nullable, defaultValue }) {
|
|
1869
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1870
|
+
type: "blob",
|
|
1871
|
+
primitive: "string",
|
|
1872
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1674
1873
|
});
|
|
1675
1874
|
}
|
|
1676
1875
|
/**
|
|
1677
|
-
*
|
|
1876
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
1678
1877
|
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
|
|
1878
|
+
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
|
|
1879
|
+
* falls through and handles it as that single type with nullability already folded in.
|
|
1880
|
+
*/
|
|
1881
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1882
|
+
const types = schema.type;
|
|
1883
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
1884
|
+
if (nonNullTypes.length <= 1) return null;
|
|
1885
|
+
const arrayNullable = types.includes("null") || nullable || void 0;
|
|
1886
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1887
|
+
type: "union",
|
|
1888
|
+
members: nonNullTypes.map((t) => parseSchema({
|
|
1889
|
+
schema: {
|
|
1890
|
+
...schema,
|
|
1891
|
+
type: t
|
|
1892
|
+
},
|
|
1893
|
+
name
|
|
1894
|
+
}, rawOptions)),
|
|
1895
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue)
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1900
|
+
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
1901
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
1902
|
+
* match/convert/fall-through contract.
|
|
1903
|
+
*/
|
|
1904
|
+
const schemaRules = [
|
|
1905
|
+
{
|
|
1906
|
+
match: ({ schema }) => dialect.schema.isReference(schema),
|
|
1907
|
+
convert: convertRef
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
match: ({ schema }) => !!schema.allOf?.length,
|
|
1911
|
+
convert: convertAllOf
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
|
|
1915
|
+
convert: convertUnion
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
match: ({ schema }) => "const" in schema && schema.const !== void 0,
|
|
1919
|
+
convert: convertConst
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
match: ({ schema }) => !!schema.format,
|
|
1923
|
+
convert: convertFormat
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
match: ({ schema }) => dialect.schema.isBinary(schema),
|
|
1927
|
+
convert: convertBlob
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
|
|
1931
|
+
convert: convertMultiType
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
|
|
1935
|
+
convert: convertString
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
|
|
1939
|
+
convert: (ctx) => convertNumeric(ctx, "number")
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
match: ({ schema }) => !!schema.enum?.length,
|
|
1943
|
+
convert: convertEnum
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
|
|
1947
|
+
convert: convertObject
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
match: ({ schema }) => "prefixItems" in schema,
|
|
1951
|
+
convert: convertTuple
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
match: ({ schema, type }) => type === "array" || "items" in schema,
|
|
1955
|
+
convert: convertArray
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
match: ({ type }) => type === "string",
|
|
1959
|
+
convert: convertString
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
match: ({ type }) => type === "number",
|
|
1963
|
+
convert: (ctx) => convertNumeric(ctx, "number")
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
match: ({ type }) => type === "integer",
|
|
1967
|
+
convert: (ctx) => convertNumeric(ctx, "integer")
|
|
1968
|
+
},
|
|
1969
|
+
{
|
|
1970
|
+
match: ({ type }) => type === "boolean",
|
|
1971
|
+
convert: convertBoolean
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
match: ({ type }) => type === "null",
|
|
1975
|
+
convert: ({ schema, name, nullable }) => createNullSchema(schema, name, nullable)
|
|
1976
|
+
}
|
|
1977
|
+
];
|
|
1978
|
+
/**
|
|
1979
|
+
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1980
|
+
*
|
|
1981
|
+
* Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
|
|
1982
|
+
* the first converter that produces a node. When none match, falls back to the configured
|
|
1983
|
+
* `emptySchemaType`.
|
|
1682
1984
|
*/
|
|
1683
1985
|
function parseSchema({ schema, name }, rawOptions) {
|
|
1684
1986
|
const options = {
|
|
@@ -1690,81 +1992,53 @@ function createSchemaParser(ctx) {
|
|
|
1690
1992
|
schema: flattenedSchema,
|
|
1691
1993
|
name
|
|
1692
1994
|
}, rawOptions);
|
|
1693
|
-
const nullable = isNullable(schema) || void 0;
|
|
1694
|
-
const
|
|
1695
|
-
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
1696
|
-
const ctx = {
|
|
1995
|
+
const nullable = dialect.schema.isNullable(schema) || void 0;
|
|
1996
|
+
const schemaCtx = {
|
|
1697
1997
|
schema,
|
|
1698
1998
|
name,
|
|
1699
1999
|
nullable,
|
|
1700
|
-
defaultValue,
|
|
1701
|
-
type,
|
|
2000
|
+
defaultValue: schema.default === null && nullable ? void 0 : schema.default,
|
|
2001
|
+
type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
|
|
1702
2002
|
rawOptions,
|
|
1703
2003
|
options
|
|
1704
2004
|
};
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
if (schema.format) {
|
|
1710
|
-
const formatResult = convertFormat(ctx);
|
|
1711
|
-
if (formatResult) return formatResult;
|
|
1712
|
-
}
|
|
1713
|
-
if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return _kubb_core.ast.createSchema({
|
|
1714
|
-
type: "blob",
|
|
1715
|
-
primitive: "string",
|
|
1716
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1717
|
-
});
|
|
1718
|
-
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
1719
|
-
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
1720
|
-
const arrayNullable = schema.type.includes("null") || nullable || void 0;
|
|
1721
|
-
if (nonNullTypes.length > 1) return _kubb_core.ast.createSchema({
|
|
1722
|
-
type: "union",
|
|
1723
|
-
members: nonNullTypes.map((t) => parseSchema({
|
|
1724
|
-
schema: {
|
|
1725
|
-
...schema,
|
|
1726
|
-
type: t
|
|
1727
|
-
},
|
|
1728
|
-
name
|
|
1729
|
-
}, rawOptions)),
|
|
1730
|
-
...buildSchemaNode(schema, name, arrayNullable, defaultValue)
|
|
1731
|
-
});
|
|
2005
|
+
for (const rule of schemaRules) {
|
|
2006
|
+
if (!rule.match(schemaCtx)) continue;
|
|
2007
|
+
const node = rule.convert(schemaCtx);
|
|
2008
|
+
if (node) return node;
|
|
1732
2009
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (schema.minimum !== void 0 || schema.maximum !== void 0) return convertNumeric(ctx, "number");
|
|
1736
|
-
}
|
|
1737
|
-
if (schema.enum?.length) return convertEnum(ctx);
|
|
1738
|
-
if (type === "object" || schema.properties || schema.additionalProperties || "patternProperties" in schema) return convertObject(ctx);
|
|
1739
|
-
if ("prefixItems" in schema) return convertTuple(ctx);
|
|
1740
|
-
if (type === "array" || "items" in schema) return convertArray(ctx);
|
|
1741
|
-
if (type === "string") return convertString(ctx);
|
|
1742
|
-
if (type === "number") return convertNumeric(ctx, "number");
|
|
1743
|
-
if (type === "integer") return convertNumeric(ctx, "integer");
|
|
1744
|
-
if (type === "boolean") return convertBoolean(ctx);
|
|
1745
|
-
if (type === "null") return convertNull(ctx);
|
|
1746
|
-
const emptyType = typeOptionMap.get(options.emptySchemaType);
|
|
1747
|
-
return _kubb_core.ast.createSchema({
|
|
2010
|
+
const emptyType = options.emptySchemaType;
|
|
2011
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
1748
2012
|
type: emptyType,
|
|
1749
2013
|
name,
|
|
1750
2014
|
title: schema.title,
|
|
1751
|
-
description: schema.description
|
|
2015
|
+
description: schema.description,
|
|
2016
|
+
format: schema.format
|
|
1752
2017
|
});
|
|
1753
2018
|
}
|
|
1754
2019
|
/**
|
|
1755
2020
|
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
1756
2021
|
*/
|
|
1757
|
-
function parseParameter(options, param) {
|
|
2022
|
+
function parseParameter(options, param, parentName) {
|
|
1758
2023
|
const required = param["required"] ?? false;
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
2024
|
+
const paramName = param["name"];
|
|
2025
|
+
const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
|
|
2026
|
+
const schema = param["schema"] ? parseSchema({
|
|
2027
|
+
schema: param["schema"],
|
|
2028
|
+
name: schemaName
|
|
2029
|
+
}, options) : _kubb_ast.ast.factory.createSchema({ type: options.unknownType });
|
|
2030
|
+
const style = param["style"];
|
|
2031
|
+
const explode = param["explode"];
|
|
2032
|
+
return _kubb_ast.ast.factory.createParameter({
|
|
2033
|
+
name: paramName,
|
|
1762
2034
|
in: param["in"],
|
|
1763
2035
|
schema: {
|
|
1764
2036
|
...schema,
|
|
1765
2037
|
description: param["description"] ?? schema.description
|
|
1766
2038
|
},
|
|
1767
|
-
required
|
|
2039
|
+
required,
|
|
2040
|
+
...style !== void 0 ? { style } : {},
|
|
2041
|
+
...explode !== void 0 ? { explode } : {}
|
|
1768
2042
|
});
|
|
1769
2043
|
}
|
|
1770
2044
|
/**
|
|
@@ -1780,73 +2054,97 @@ function createSchemaParser(ctx) {
|
|
|
1780
2054
|
};
|
|
1781
2055
|
}
|
|
1782
2056
|
/**
|
|
1783
|
-
* Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
|
|
1784
|
-
*/
|
|
1785
|
-
function getResponseMeta(responseObj) {
|
|
1786
|
-
if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
|
|
1787
|
-
const inline = responseObj;
|
|
1788
|
-
return {
|
|
1789
|
-
description: inline.description,
|
|
1790
|
-
content: inline.content
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
1793
|
-
/**
|
|
1794
2057
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
1795
2058
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
1796
2059
|
*/
|
|
1797
2060
|
function collectPropertyKeysByFlag(schema, flag) {
|
|
1798
|
-
if (!schema?.properties) return
|
|
2061
|
+
if (!schema?.properties) return null;
|
|
1799
2062
|
const keys = [];
|
|
1800
2063
|
for (const key in schema.properties) {
|
|
1801
2064
|
const prop = schema.properties[key];
|
|
1802
|
-
if (prop && !isReference(prop) && prop[flag]) keys.push(key);
|
|
2065
|
+
if (prop && !dialect.schema.isReference(prop) && prop[flag]) keys.push(key);
|
|
1803
2066
|
}
|
|
1804
|
-
return keys.length ? keys :
|
|
2067
|
+
return keys.length ? keys : null;
|
|
1805
2068
|
}
|
|
1806
2069
|
/**
|
|
1807
2070
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1808
2071
|
*/
|
|
1809
2072
|
function parseOperation(options, operation) {
|
|
1810
|
-
const
|
|
2073
|
+
const operationId = getOperationId(operation);
|
|
2074
|
+
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
2075
|
+
const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
|
|
1811
2076
|
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
|
|
1812
2077
|
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
2078
|
+
const requestBodyName = operationName ? `${operationName}Request` : void 0;
|
|
1813
2079
|
const content = allContentTypes.flatMap((ct) => {
|
|
1814
2080
|
const schema = getRequestSchema(document, operation, { contentType: ct });
|
|
1815
2081
|
if (!schema) return [];
|
|
1816
|
-
return [{
|
|
2082
|
+
return [_kubb_ast.ast.factory.createContent({
|
|
1817
2083
|
contentType: ct,
|
|
1818
|
-
schema:
|
|
2084
|
+
schema: _kubb_ast.ast.optionality(parseSchema({
|
|
2085
|
+
schema,
|
|
2086
|
+
name: requestBodyName
|
|
2087
|
+
}, options), requestBodyMeta.required),
|
|
1819
2088
|
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1820
|
-
}];
|
|
2089
|
+
})];
|
|
1821
2090
|
});
|
|
1822
2091
|
const requestBody = content.length > 0 || requestBodyMeta.description ? {
|
|
1823
2092
|
description: requestBodyMeta.description,
|
|
1824
2093
|
required: requestBodyMeta.required || void 0,
|
|
1825
2094
|
content: content.length > 0 ? content : void 0
|
|
1826
2095
|
} : void 0;
|
|
1827
|
-
const responses =
|
|
1828
|
-
const responseObj =
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2096
|
+
const responses = getResponseStatusCodes(operation).map((statusCode) => {
|
|
2097
|
+
const responseObj = getResponseByStatusCode({
|
|
2098
|
+
document,
|
|
2099
|
+
operation,
|
|
2100
|
+
statusCode
|
|
2101
|
+
});
|
|
2102
|
+
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
2103
|
+
const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
|
|
2104
|
+
const parseEntrySchema = (contentType) => {
|
|
2105
|
+
const raw = getResponseSchema(document, operation, statusCode, { contentType });
|
|
2106
|
+
return {
|
|
2107
|
+
schema: raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
2108
|
+
schema: raw,
|
|
2109
|
+
name: responseName
|
|
2110
|
+
}, options) : _kubb_ast.ast.factory.createSchema({ type: options.emptySchemaType }),
|
|
2111
|
+
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
2114
|
+
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => _kubb_ast.ast.factory.createContent({
|
|
2115
|
+
contentType,
|
|
2116
|
+
...parseEntrySchema(contentType)
|
|
2117
|
+
}));
|
|
2118
|
+
if (content.length === 0) content.push(_kubb_ast.ast.factory.createContent({
|
|
2119
|
+
contentType: getRequestContentType({
|
|
2120
|
+
document,
|
|
2121
|
+
operation
|
|
2122
|
+
}) || "application/json",
|
|
2123
|
+
...parseEntrySchema(ctx.contentType)
|
|
2124
|
+
}));
|
|
2125
|
+
return _kubb_ast.ast.factory.createResponse({
|
|
1834
2126
|
statusCode,
|
|
1835
2127
|
description,
|
|
1836
|
-
|
|
1837
|
-
mediaType,
|
|
1838
|
-
keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
|
|
2128
|
+
content
|
|
1839
2129
|
});
|
|
1840
2130
|
});
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
2131
|
+
const pathItem = document.paths?.[operation.path];
|
|
2132
|
+
const pathItemDoc = pathItem && !dialect.schema.isReference(pathItem) ? pathItem : void 0;
|
|
2133
|
+
const pickDoc = (key) => {
|
|
2134
|
+
const own = operation.schema[key];
|
|
2135
|
+
if (typeof own === "string") return own;
|
|
2136
|
+
const fallback = pathItemDoc?.[key];
|
|
2137
|
+
return typeof fallback === "string" ? fallback : void 0;
|
|
2138
|
+
};
|
|
2139
|
+
return _kubb_ast.ast.factory.createOperation({
|
|
2140
|
+
operationId,
|
|
2141
|
+
protocol: "http",
|
|
1844
2142
|
method: operation.method.toUpperCase(),
|
|
1845
|
-
path:
|
|
1846
|
-
tags: operation.
|
|
1847
|
-
summary:
|
|
1848
|
-
description:
|
|
1849
|
-
deprecated: operation.
|
|
2143
|
+
path: operation.path,
|
|
2144
|
+
tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
|
|
2145
|
+
summary: pickDoc("summary") || void 0,
|
|
2146
|
+
description: pickDoc("description") || void 0,
|
|
2147
|
+
deprecated: operation.schema.deprecated || void 0,
|
|
1850
2148
|
parameters,
|
|
1851
2149
|
requestBody,
|
|
1852
2150
|
responses
|
|
@@ -1858,59 +2156,122 @@ function createSchemaParser(ctx) {
|
|
|
1858
2156
|
parseParameter
|
|
1859
2157
|
};
|
|
1860
2158
|
}
|
|
2159
|
+
//#endregion
|
|
2160
|
+
//#region src/promoteEnums.ts
|
|
1861
2161
|
/**
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here —
|
|
1866
|
-
* the tree is a pure data structure representing all schemas and operations.
|
|
1867
|
-
*
|
|
1868
|
-
* Returns the AST root and a `nameMapping` for resolving schema references.
|
|
1869
|
-
*
|
|
1870
|
-
* @example
|
|
1871
|
-
* ```ts
|
|
1872
|
-
* import { parseOas } from '@kubb/adapter-oas'
|
|
1873
|
-
*
|
|
1874
|
-
* const document = await parseFromConfig(config)
|
|
1875
|
-
* const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
|
|
1876
|
-
* ```
|
|
2162
|
+
* Collects inline enums to lift to the top level, keyed by the name the parser derived for them
|
|
2163
|
+
* (e.g. `PetStatusEnum`). An enum already defined as a top-level component is left as-is, and a
|
|
2164
|
+
* name that recurs maps to the first definition so each name yields one shared type.
|
|
1877
2165
|
*/
|
|
1878
|
-
function
|
|
1879
|
-
const
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2166
|
+
function collectInlineEnums(roots, topLevelNames) {
|
|
2167
|
+
const promoted = /* @__PURE__ */ new Map();
|
|
2168
|
+
for (const root of roots) {
|
|
2169
|
+
const isSchemaRoot = root.kind === "Schema";
|
|
2170
|
+
for (const node of _kubb_ast.ast.collect(root, { schema: (schemaNode) => schemaNode })) {
|
|
2171
|
+
if (node.type !== "enum" || !node.name) continue;
|
|
2172
|
+
if (isSchemaRoot && node === root) continue;
|
|
2173
|
+
if (topLevelNames.has(node.name)) continue;
|
|
2174
|
+
if (!promoted.has(node.name)) promoted.set(node.name, {
|
|
2175
|
+
...node,
|
|
2176
|
+
optional: void 0,
|
|
2177
|
+
nullish: void 0
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return promoted;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Replaces every promoted inline enum in `node` with a `ref` to its lifted definition, keeping the
|
|
2185
|
+
* occurrence's usage-slot and documentation fields.
|
|
2186
|
+
*/
|
|
2187
|
+
function refPromotedEnums(node, promoted) {
|
|
2188
|
+
if (promoted.size === 0) return node;
|
|
2189
|
+
return _kubb_ast.ast.transform(node, { schema(schemaNode) {
|
|
2190
|
+
if (schemaNode.type !== "enum" || !schemaNode.name || !promoted.has(schemaNode.name)) return void 0;
|
|
2191
|
+
return _kubb_ast.ast.factory.createSchema({
|
|
2192
|
+
type: "ref",
|
|
2193
|
+
name: schemaNode.name,
|
|
2194
|
+
ref: `${SCHEMA_REF_PREFIX}${schemaNode.name}`,
|
|
2195
|
+
optional: schemaNode.optional,
|
|
2196
|
+
nullish: schemaNode.nullish,
|
|
2197
|
+
readOnly: schemaNode.readOnly,
|
|
2198
|
+
writeOnly: schemaNode.writeOnly,
|
|
2199
|
+
deprecated: schemaNode.deprecated,
|
|
2200
|
+
description: schemaNode.description,
|
|
2201
|
+
default: schemaNode.default,
|
|
2202
|
+
examples: schemaNode.examples
|
|
2203
|
+
});
|
|
2204
|
+
} });
|
|
2205
|
+
}
|
|
2206
|
+
//#endregion
|
|
2207
|
+
//#region src/schemaDiagnostics.ts
|
|
2208
|
+
/**
|
|
2209
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
2210
|
+
* top-level schema. Walks the node the parser produced, threading the RFC 6901
|
|
2211
|
+
* pointer as it descends so a nested field reports against its full path
|
|
2212
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
2213
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
2214
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
2215
|
+
*/
|
|
2216
|
+
function reportSchemaDiagnostics({ node, name }) {
|
|
2217
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`);
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
2221
|
+
* property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
|
|
2222
|
+
*/
|
|
2223
|
+
function escapePointerToken(token) {
|
|
2224
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2225
|
+
}
|
|
2226
|
+
function visit(node, pointer) {
|
|
2227
|
+
if (node.deprecated) _kubb_core.Diagnostics.report({
|
|
2228
|
+
code: _kubb_core.Diagnostics.code.deprecated,
|
|
2229
|
+
severity: "info",
|
|
2230
|
+
message: "This schema is marked as deprecated.",
|
|
2231
|
+
location: {
|
|
2232
|
+
kind: "schema",
|
|
2233
|
+
pointer
|
|
2234
|
+
}
|
|
1888
2235
|
});
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2236
|
+
if (typeof node.format === "string" && !isHandledFormat(node.format)) _kubb_core.Diagnostics.report({
|
|
2237
|
+
code: _kubb_core.Diagnostics.code.unsupportedFormat,
|
|
2238
|
+
severity: "warning",
|
|
2239
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
2240
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
2241
|
+
location: {
|
|
2242
|
+
kind: "schema",
|
|
2243
|
+
pointer
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
if (node.type === "object") {
|
|
2247
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
|
|
2248
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (node.type === "array") {
|
|
2252
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (node.type === "tuple") {
|
|
2256
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
|
|
1902
2260
|
}
|
|
1903
2261
|
//#endregion
|
|
1904
2262
|
//#region src/adapter.ts
|
|
1905
2263
|
/**
|
|
1906
|
-
*
|
|
2264
|
+
* The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
|
|
1907
2265
|
*/
|
|
1908
2266
|
const adapterOasName = "oas";
|
|
1909
2267
|
/**
|
|
1910
|
-
*
|
|
2268
|
+
* Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
|
|
2269
|
+
* spec from `input` (a file path, URL, inline content, or parsed object), validates
|
|
2270
|
+
* it, resolves the base URL, and converts every schema and operation into the
|
|
2271
|
+
* universal AST that every downstream plugin consumes.
|
|
1911
2272
|
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
2273
|
+
* Configure once on `defineConfig`. The adapter's choices (date representation,
|
|
2274
|
+
* integer width, server URL) apply to every plugin in the build.
|
|
1914
2275
|
*
|
|
1915
2276
|
* @example
|
|
1916
2277
|
* ```ts
|
|
@@ -1919,26 +2280,138 @@ const adapterOasName = "oas";
|
|
|
1919
2280
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1920
2281
|
*
|
|
1921
2282
|
* export default defineConfig({
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
2283
|
+
* input: './petStore.yaml',
|
|
2284
|
+
* output: { path: './src/gen' },
|
|
2285
|
+
* adapter: adapterOas({
|
|
2286
|
+
* server: { index: 0 },
|
|
2287
|
+
* discriminator: 'propagate',
|
|
2288
|
+
* dateType: 'date',
|
|
2289
|
+
* }),
|
|
1924
2290
|
* plugins: [pluginTs()],
|
|
1925
2291
|
* })
|
|
1926
2292
|
* ```
|
|
1927
2293
|
*/
|
|
1928
2294
|
const adapterOas = (0, _kubb_core.createAdapter)((options) => {
|
|
1929
|
-
const { validate = true, contentType,
|
|
2295
|
+
const { validate = true, contentType, server, discriminator = "preserve", enums = "inline", dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
|
|
2296
|
+
const parserOptions = {
|
|
2297
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
2298
|
+
dateType,
|
|
2299
|
+
integerType,
|
|
2300
|
+
unknownType,
|
|
2301
|
+
emptySchemaType,
|
|
2302
|
+
enumSuffix
|
|
2303
|
+
};
|
|
1930
2304
|
let nameMapping = /* @__PURE__ */ new Map();
|
|
1931
|
-
let parsedDocument;
|
|
1932
|
-
|
|
2305
|
+
let parsedDocument = null;
|
|
2306
|
+
const documentCache = /* @__PURE__ */ new WeakMap();
|
|
2307
|
+
const schemasCache = /* @__PURE__ */ new WeakMap();
|
|
2308
|
+
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2309
|
+
function ensureDocument(source) {
|
|
2310
|
+
const cached = documentCache.get(source);
|
|
2311
|
+
if (cached) return cached;
|
|
2312
|
+
const promise = (async () => {
|
|
2313
|
+
const fresh = await parseFromConfig(source);
|
|
2314
|
+
if (validate) await validateDocument(fresh);
|
|
2315
|
+
parsedDocument = fresh;
|
|
2316
|
+
return fresh;
|
|
2317
|
+
})();
|
|
2318
|
+
documentCache.set(source, promise);
|
|
2319
|
+
return promise;
|
|
2320
|
+
}
|
|
2321
|
+
function ensureSchemas(document) {
|
|
2322
|
+
const cached = schemasCache.get(document);
|
|
2323
|
+
if (cached) return cached;
|
|
2324
|
+
const promise = Promise.resolve().then(() => {
|
|
2325
|
+
const result = getSchemas(document, { contentType });
|
|
2326
|
+
nameMapping = result.nameMapping;
|
|
2327
|
+
return result.schemas;
|
|
2328
|
+
});
|
|
2329
|
+
schemasCache.set(document, promise);
|
|
2330
|
+
return promise;
|
|
2331
|
+
}
|
|
2332
|
+
function ensureSchemaParser(document) {
|
|
2333
|
+
const cached = schemaParserCache.get(document);
|
|
2334
|
+
if (cached) return cached;
|
|
2335
|
+
const parser = createSchemaParser({
|
|
2336
|
+
document,
|
|
2337
|
+
contentType
|
|
2338
|
+
});
|
|
2339
|
+
schemaParserCache.set(document, parser);
|
|
2340
|
+
return parser;
|
|
2341
|
+
}
|
|
2342
|
+
function parseInput({ document, schemas, parser }) {
|
|
2343
|
+
const { parseSchema, parseOperation } = parser;
|
|
2344
|
+
const parsedByName = /* @__PURE__ */ new Map();
|
|
2345
|
+
const refAliasMap = /* @__PURE__ */ new Map();
|
|
2346
|
+
const enumNames = [];
|
|
2347
|
+
const discriminatorParentNodes = [];
|
|
2348
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
2349
|
+
const node = parseSchema({
|
|
2350
|
+
schema,
|
|
2351
|
+
name
|
|
2352
|
+
}, parserOptions);
|
|
2353
|
+
parsedByName.set(name, node);
|
|
2354
|
+
reportSchemaDiagnostics({
|
|
2355
|
+
node,
|
|
2356
|
+
name
|
|
2357
|
+
});
|
|
2358
|
+
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2359
|
+
if ((0, _kubb_ast.narrowSchema)(node, "enum") && node.name) enumNames.push(node.name);
|
|
2360
|
+
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2361
|
+
}
|
|
2362
|
+
const circularNames = [...(0, _kubb_ast.findCircularSchemas)([...parsedByName.values()])];
|
|
2363
|
+
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2364
|
+
const operationNodes = [];
|
|
2365
|
+
for (const operation of getOperations(document)) {
|
|
2366
|
+
const operationNode = parseOperation(parserOptions, operation);
|
|
2367
|
+
if (operationNode) operationNodes.push(operationNode);
|
|
2368
|
+
}
|
|
2369
|
+
let promotedEnums = null;
|
|
2370
|
+
if (enums === "root") {
|
|
2371
|
+
promotedEnums = collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas)));
|
|
2372
|
+
for (const name of promotedEnums.keys()) enumNames.push(name);
|
|
2373
|
+
}
|
|
2374
|
+
const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
|
|
2375
|
+
for (const name of Object.keys(schemas)) {
|
|
2376
|
+
const alias = refAliasMap.get(name);
|
|
2377
|
+
let node;
|
|
2378
|
+
if (alias?.name && parsedByName.has(alias.name)) node = {
|
|
2379
|
+
...parsedByName.get(alias.name),
|
|
2380
|
+
name
|
|
2381
|
+
};
|
|
2382
|
+
else {
|
|
2383
|
+
const parsed = parsedByName.get(name);
|
|
2384
|
+
const child = discriminatorChildMap?.get(name);
|
|
2385
|
+
node = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2386
|
+
}
|
|
2387
|
+
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2388
|
+
}
|
|
2389
|
+
const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
|
|
2390
|
+
return _kubb_ast.ast.factory.createInput({
|
|
2391
|
+
schemas: schemaNodes,
|
|
2392
|
+
operations,
|
|
2393
|
+
meta: {
|
|
2394
|
+
title: document.info?.title,
|
|
2395
|
+
description: document.info?.description,
|
|
2396
|
+
version: document.info?.version,
|
|
2397
|
+
baseURL: resolveBaseUrl({
|
|
2398
|
+
document,
|
|
2399
|
+
server
|
|
2400
|
+
}),
|
|
2401
|
+
circularNames,
|
|
2402
|
+
enumNames
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
1933
2406
|
return {
|
|
1934
2407
|
name: "oas",
|
|
1935
2408
|
get options() {
|
|
1936
2409
|
return {
|
|
1937
2410
|
validate,
|
|
1938
2411
|
contentType,
|
|
1939
|
-
|
|
1940
|
-
serverVariables,
|
|
2412
|
+
server,
|
|
1941
2413
|
discriminator,
|
|
2414
|
+
enums,
|
|
1942
2415
|
dateType,
|
|
1943
2416
|
integerType,
|
|
1944
2417
|
unknownType,
|
|
@@ -1950,71 +2423,34 @@ const adapterOas = (0, _kubb_core.createAdapter)((options) => {
|
|
|
1950
2423
|
get document() {
|
|
1951
2424
|
return parsedDocument;
|
|
1952
2425
|
},
|
|
1953
|
-
get inputNode() {
|
|
1954
|
-
return inputNode;
|
|
1955
|
-
},
|
|
1956
2426
|
async validate(input, options) {
|
|
2427
|
+
await assertInputExists(input);
|
|
1957
2428
|
await validateDocument(await parseDocument(input), options);
|
|
1958
2429
|
},
|
|
1959
2430
|
getImports(node, resolve) {
|
|
1960
|
-
return
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
resolve
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
});
|
|
2431
|
+
return (0, _kubb_ast.collect)(node, { schema(schemaNode) {
|
|
2432
|
+
const schemaRef = (0, _kubb_ast.narrowSchema)(schemaNode, "ref");
|
|
2433
|
+
if (!schemaRef?.ref) return null;
|
|
2434
|
+
const result = resolve(nameMapping.get(schemaRef.ref) ?? (0, _kubb_ast.extractRefName)(schemaRef.ref));
|
|
2435
|
+
if (!result) return null;
|
|
2436
|
+
return _kubb_ast.ast.factory.createImport({
|
|
2437
|
+
name: [result.name],
|
|
2438
|
+
path: result.path
|
|
2439
|
+
});
|
|
2440
|
+
} });
|
|
1972
2441
|
},
|
|
1973
2442
|
async parse(source) {
|
|
1974
|
-
const document = await
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
contentType,
|
|
1980
|
-
dateType,
|
|
1981
|
-
integerType,
|
|
1982
|
-
unknownType,
|
|
1983
|
-
emptySchemaType,
|
|
1984
|
-
enumSuffix
|
|
1985
|
-
});
|
|
1986
|
-
const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
|
|
1987
|
-
nameMapping = parsedNameMapping;
|
|
1988
|
-
parsedDocument = document;
|
|
1989
|
-
inputNode = _kubb_core.ast.createInput({
|
|
1990
|
-
...node,
|
|
1991
|
-
meta: {
|
|
1992
|
-
title: document.info?.title,
|
|
1993
|
-
description: document.info?.description,
|
|
1994
|
-
version: document.info?.version,
|
|
1995
|
-
baseURL
|
|
1996
|
-
}
|
|
2443
|
+
const document = await ensureDocument(source);
|
|
2444
|
+
return parseInput({
|
|
2445
|
+
document,
|
|
2446
|
+
schemas: await ensureSchemas(document),
|
|
2447
|
+
parser: ensureSchemaParser(document)
|
|
1997
2448
|
});
|
|
1998
|
-
return inputNode;
|
|
1999
2449
|
}
|
|
2000
2450
|
};
|
|
2001
2451
|
});
|
|
2002
2452
|
//#endregion
|
|
2003
|
-
//#region src/types.ts
|
|
2004
|
-
/**
|
|
2005
|
-
* Maps uppercase HTTP method names to lowercase for backwards compatibility.
|
|
2006
|
-
*
|
|
2007
|
-
* @example
|
|
2008
|
-
* ```ts
|
|
2009
|
-
* HttpMethods['GET'] // 'get'
|
|
2010
|
-
* HttpMethods['POST'] // 'post'
|
|
2011
|
-
* ```
|
|
2012
|
-
*/
|
|
2013
|
-
const HttpMethods = Object.fromEntries(Object.entries(_kubb_core.ast.httpMethods).map(([lower, upper]) => [upper, lower]));
|
|
2014
|
-
//#endregion
|
|
2015
|
-
exports.HttpMethods = HttpMethods;
|
|
2016
2453
|
exports.adapterOas = adapterOas;
|
|
2017
2454
|
exports.adapterOasName = adapterOasName;
|
|
2018
|
-
exports.mergeDocuments = mergeDocuments;
|
|
2019
2455
|
|
|
2020
2456
|
//# sourceMappingURL=index.cjs.map
|