@kubb/adapter-oas 5.0.0-alpha.16 → 5.0.0-alpha.18
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/dist/index.cjs +796 -925
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +71 -148
- package/dist/index.js +796 -921
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/adapter.ts +62 -69
- package/src/constants.ts +56 -17
- package/src/discriminator.ts +95 -21
- package/src/factory.ts +151 -0
- package/src/guards.ts +68 -0
- package/src/parser.ts +222 -403
- package/src/refs.ts +57 -0
- package/src/resolvers.ts +490 -0
- package/src/types.ts +159 -48
- package/src/infer.ts +0 -85
- package/src/naming.ts +0 -25
- package/src/oas/Oas.ts +0 -615
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -77
- package/src/oas/utils.ts +0 -401
- package/src/refResolver.ts +0 -65
package/dist/index.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { applyDiscriminatorEnum, collect, createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, extractRefName, mediaTypes, mergeAdjacentAnonymousObjects, narrowSchema, schemaTypes, simplifyUnionMembers, transform } from "@kubb/ast";
|
|
1
|
+
import "./chunk--u3MIqq1.js";
|
|
2
|
+
import { childName, collectImports, createDiscriminantNode, createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, enumPropName, extractRefName, findDiscriminator, mediaTypes, mergeAdjacentObjects, narrowSchema, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, transform } from "@kubb/ast";
|
|
4
3
|
import { createAdapter } from "@kubb/core";
|
|
4
|
+
import path from "node:path";
|
|
5
5
|
import { bundle, loadConfig } from "@redocly/openapi-core";
|
|
6
6
|
import yaml from "@stoplight/yaml";
|
|
7
|
-
import { isRef } from "oas/types";
|
|
8
7
|
import OASNormalize from "oas-normalize";
|
|
9
8
|
import { isPlainObject, mergeDeep } from "remeda";
|
|
10
9
|
import swagger2openapi from "swagger2openapi";
|
|
11
|
-
import jsonpointer from "jsonpointer";
|
|
12
10
|
import BaseOas from "oas";
|
|
11
|
+
import jsonpointer from "jsonpointer";
|
|
12
|
+
import { isRef } from "oas/types";
|
|
13
13
|
import { matchesMimeType } from "oas/utils";
|
|
14
14
|
//#region src/constants.ts
|
|
15
15
|
/**
|
|
16
|
-
* Default
|
|
16
|
+
* Default parser options applied when no explicit options are provided.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
21
|
+
*
|
|
22
|
+
* const parser = createOasParser(oas)
|
|
23
|
+
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
24
|
+
* ```
|
|
17
25
|
*/
|
|
18
26
|
const DEFAULT_PARSER_OPTIONS = {
|
|
19
27
|
dateType: "string",
|
|
@@ -23,21 +31,30 @@ const DEFAULT_PARSER_OPTIONS = {
|
|
|
23
31
|
enumSuffix: "enum"
|
|
24
32
|
};
|
|
25
33
|
/**
|
|
26
|
-
* OpenAPI version string written into
|
|
34
|
+
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
27
35
|
*/
|
|
28
36
|
const MERGE_OPENAPI_VERSION = "3.0.0";
|
|
29
37
|
/**
|
|
30
|
-
* Fallback `info.title`
|
|
38
|
+
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
31
39
|
*/
|
|
32
40
|
const MERGE_DEFAULT_TITLE = "Merged API";
|
|
33
41
|
/**
|
|
34
|
-
* Fallback `info.version`
|
|
42
|
+
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
35
43
|
*/
|
|
36
44
|
const MERGE_DEFAULT_VERSION = "1.0.0";
|
|
37
45
|
/**
|
|
38
|
-
* JSON Schema keywords that
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
47
|
+
*
|
|
48
|
+
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
49
|
+
* intersection member rather than being merged into the parent.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
54
|
+
*
|
|
55
|
+
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
56
|
+
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
57
|
+
* ```
|
|
41
58
|
*/
|
|
42
59
|
const structuralKeys = new Set([
|
|
43
60
|
"properties",
|
|
@@ -49,14 +66,20 @@ const structuralKeys = new Set([
|
|
|
49
66
|
"not"
|
|
50
67
|
]);
|
|
51
68
|
/**
|
|
52
|
-
*
|
|
69
|
+
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
53
70
|
*
|
|
54
|
-
* Only formats
|
|
55
|
-
* `int64`, `date-time`, `date`,
|
|
56
|
-
*
|
|
71
|
+
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
72
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
|
|
73
|
+
* in the parser. `ipv4`, `ipv6`, and `hostname` map to `'url'` as the closest supported scalar.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { formatMap } from '@kubb/adapter-oas'
|
|
57
78
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
79
|
+
* formatMap['uuid'] // 'uuid'
|
|
80
|
+
* formatMap['binary'] // 'blob'
|
|
81
|
+
* formatMap['float'] // 'number'
|
|
82
|
+
* ```
|
|
60
83
|
*/
|
|
61
84
|
const formatMap = {
|
|
62
85
|
uuid: "uuid",
|
|
@@ -76,32 +99,104 @@ const formatMap = {
|
|
|
76
99
|
double: "number"
|
|
77
100
|
};
|
|
78
101
|
/**
|
|
79
|
-
* Vendor extension keys
|
|
80
|
-
*
|
|
102
|
+
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
107
|
+
*
|
|
108
|
+
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
109
|
+
* ```
|
|
81
110
|
*/
|
|
82
111
|
const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
112
|
+
/**
|
|
113
|
+
* Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
|
|
114
|
+
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
115
|
+
*/
|
|
116
|
+
const typeOptionMap = new Map([
|
|
117
|
+
["any", schemaTypes.any],
|
|
118
|
+
["unknown", schemaTypes.unknown],
|
|
119
|
+
["void", schemaTypes.void]
|
|
120
|
+
]);
|
|
83
121
|
//#endregion
|
|
84
|
-
//#region src/
|
|
122
|
+
//#region src/discriminator.ts
|
|
85
123
|
/**
|
|
86
|
-
*
|
|
124
|
+
* Injects discriminator enum values into child schemas so they know which value identifies them.
|
|
125
|
+
*
|
|
126
|
+
* Finds every union schema in `root.schemas` that has a `discriminatorPropertyName`, collects the
|
|
127
|
+
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
128
|
+
* child object schema.
|
|
87
129
|
*
|
|
88
|
-
*
|
|
89
|
-
* 1. `overrides[key]` — caller-supplied value.
|
|
90
|
-
* 2. `variable.default` — spec-defined default, coerced to `string`.
|
|
91
|
-
* 3. Variable is left unreplaced when neither is available.
|
|
130
|
+
* Returns a new `RootNode` — the original is never mutated.
|
|
92
131
|
*
|
|
93
|
-
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const { root } = parseOas(document, options)
|
|
135
|
+
* const next = applyDiscriminatorInheritance(root)
|
|
136
|
+
* ```
|
|
94
137
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
function applyDiscriminatorInheritance(root) {
|
|
139
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const schema of root.schemas) {
|
|
141
|
+
let unionNode = narrowSchema(schema, "union");
|
|
142
|
+
if (!unionNode) {
|
|
143
|
+
const intersectionMembers = narrowSchema(schema, "intersection")?.members;
|
|
144
|
+
if (intersectionMembers) for (const m of intersectionMembers) {
|
|
145
|
+
const u = narrowSchema(m, "union");
|
|
146
|
+
if (u) {
|
|
147
|
+
unionNode = u;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue;
|
|
153
|
+
const { discriminatorPropertyName, members } = unionNode;
|
|
154
|
+
for (const member of members) {
|
|
155
|
+
const intersectionNode = narrowSchema(member, "intersection");
|
|
156
|
+
if (!intersectionNode?.members) continue;
|
|
157
|
+
let refNode;
|
|
158
|
+
let objNode;
|
|
159
|
+
for (const m of intersectionNode.members) {
|
|
160
|
+
refNode ??= narrowSchema(m, "ref");
|
|
161
|
+
objNode ??= narrowSchema(m, "object");
|
|
162
|
+
}
|
|
163
|
+
if (!refNode?.name || !objNode) continue;
|
|
164
|
+
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
|
|
165
|
+
const enumNode = prop ? narrowSchema(prop.schema, "enum") : void 0;
|
|
166
|
+
if (!enumNode?.enumValues?.length) continue;
|
|
167
|
+
const enumValues = enumNode.enumValues.filter((v) => v !== null);
|
|
168
|
+
if (!enumValues.length) continue;
|
|
169
|
+
const existing = childMap.get(refNode.name);
|
|
170
|
+
if (existing) existing.enumValues.push(...enumValues);
|
|
171
|
+
else childMap.set(refNode.name, {
|
|
172
|
+
propertyName: discriminatorPropertyName,
|
|
173
|
+
enumValues: [...enumValues]
|
|
174
|
+
});
|
|
175
|
+
}
|
|
103
176
|
}
|
|
104
|
-
return
|
|
177
|
+
if (childMap.size === 0) return root;
|
|
178
|
+
return transform(root, { schema(node, { parent }) {
|
|
179
|
+
if (parent?.kind !== "Root" || !node.name) return;
|
|
180
|
+
const entry = childMap.get(node.name);
|
|
181
|
+
if (!entry) return;
|
|
182
|
+
const objectNode = narrowSchema(node, "object");
|
|
183
|
+
if (!objectNode) return;
|
|
184
|
+
const { propertyName, enumValues } = entry;
|
|
185
|
+
const newProp = createProperty({
|
|
186
|
+
name: propertyName,
|
|
187
|
+
required: true,
|
|
188
|
+
schema: createSchema({
|
|
189
|
+
type: "enum",
|
|
190
|
+
enumValues
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
|
|
194
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
|
|
195
|
+
return {
|
|
196
|
+
...objectNode,
|
|
197
|
+
properties: newProperties
|
|
198
|
+
};
|
|
199
|
+
} });
|
|
105
200
|
}
|
|
106
201
|
//#endregion
|
|
107
202
|
//#region ../../internals/utils/src/casing.ts
|
|
@@ -165,6 +260,13 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
165
260
|
//#region ../../internals/utils/src/reserved.ts
|
|
166
261
|
/**
|
|
167
262
|
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* isValidVarName('status') // true
|
|
267
|
+
* isValidVarName('class') // false (reserved word)
|
|
268
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
269
|
+
* ```
|
|
168
270
|
*/
|
|
169
271
|
function isValidVarName(name) {
|
|
170
272
|
try {
|
|
@@ -185,18 +287,33 @@ function isValidVarName(name) {
|
|
|
185
287
|
* p.template // '`/pet/${petId}`'
|
|
186
288
|
*/
|
|
187
289
|
var URLPath = class {
|
|
188
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
|
|
292
|
+
*/
|
|
189
293
|
path;
|
|
190
294
|
#options;
|
|
191
295
|
constructor(path, options = {}) {
|
|
192
296
|
this.path = path;
|
|
193
297
|
this.#options = options;
|
|
194
298
|
}
|
|
195
|
-
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
299
|
+
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* new URLPath('/pet/{petId}').URL // '/pet/:petId'
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
196
306
|
get URL() {
|
|
197
307
|
return this.toURLPath();
|
|
198
308
|
}
|
|
199
|
-
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
|
|
309
|
+
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
|
|
314
|
+
* new URLPath('/pet/{petId}').isURL // false
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
200
317
|
get isURL() {
|
|
201
318
|
try {
|
|
202
319
|
return !!new URL(this.path).href;
|
|
@@ -214,11 +331,25 @@ var URLPath = class {
|
|
|
214
331
|
get template() {
|
|
215
332
|
return this.toTemplateString();
|
|
216
333
|
}
|
|
217
|
-
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
334
|
+
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* new URLPath('/pet/{petId}').object
|
|
339
|
+
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
218
342
|
get object() {
|
|
219
343
|
return this.toObject();
|
|
220
344
|
}
|
|
221
|
-
/** Returns a map of path parameter names, or `undefined` when the path has no parameters.
|
|
345
|
+
/** Returns a map of path parameter names, or `undefined` when the path has no parameters.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* new URLPath('/pet/{petId}').params // { petId: 'petId' }
|
|
350
|
+
* new URLPath('/pet').params // undefined
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
222
353
|
get params() {
|
|
223
354
|
return this.getParams();
|
|
224
355
|
}
|
|
@@ -226,7 +357,9 @@ var URLPath = class {
|
|
|
226
357
|
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
227
358
|
return this.#options.casing === "camelcase" ? camelCase(param) : param;
|
|
228
359
|
}
|
|
229
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
|
|
362
|
+
*/
|
|
230
363
|
#eachParam(fn) {
|
|
231
364
|
for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
|
|
232
365
|
const raw = match[1];
|
|
@@ -263,6 +396,12 @@ var URLPath = class {
|
|
|
263
396
|
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
264
397
|
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
265
398
|
* Returns `undefined` when no path parameters are found.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* new URLPath('/pet/{petId}/tag/{tagId}').getParams()
|
|
403
|
+
* // { petId: 'petId', tagId: 'tagId' }
|
|
404
|
+
* ```
|
|
266
405
|
*/
|
|
267
406
|
getParams(replacer) {
|
|
268
407
|
const params = {};
|
|
@@ -272,364 +411,28 @@ var URLPath = class {
|
|
|
272
411
|
});
|
|
273
412
|
return Object.keys(params).length > 0 ? params : void 0;
|
|
274
413
|
}
|
|
275
|
-
/** Converts the OpenAPI path to Express-style colon syntax
|
|
276
|
-
toURLPath() {
|
|
277
|
-
return this.path.replace(/\{([^}]+)\}/g, ":$1");
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region src/oas/Oas.ts
|
|
282
|
-
/**
|
|
283
|
-
* Prefix used to create synthetic `$ref` values for anonymous (inline) discriminator schemas.
|
|
284
|
-
* The suffix is the schema index within the discriminator's `oneOf`/`anyOf` array.
|
|
285
|
-
* @example `#kubb-inline-0`
|
|
286
|
-
*/
|
|
287
|
-
const KUBB_INLINE_REF_PREFIX = "#kubb-inline-";
|
|
288
|
-
var Oas = class extends BaseOas {
|
|
289
|
-
#options = { discriminator: "strict" };
|
|
290
|
-
document;
|
|
291
|
-
constructor(document) {
|
|
292
|
-
super(document, void 0);
|
|
293
|
-
this.document = document;
|
|
294
|
-
}
|
|
295
|
-
setOptions(options) {
|
|
296
|
-
this.#options = {
|
|
297
|
-
...this.#options,
|
|
298
|
-
...options
|
|
299
|
-
};
|
|
300
|
-
if (this.#options.discriminator === "inherit") this.#applyDiscriminatorInheritance();
|
|
301
|
-
}
|
|
302
|
-
get options() {
|
|
303
|
-
return this.#options;
|
|
304
|
-
}
|
|
305
|
-
get($ref) {
|
|
306
|
-
const origRef = $ref;
|
|
307
|
-
$ref = $ref.trim();
|
|
308
|
-
if ($ref === "") return null;
|
|
309
|
-
if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
310
|
-
else return null;
|
|
311
|
-
const current = jsonpointer.get(this.api, $ref);
|
|
312
|
-
if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
|
|
313
|
-
return current;
|
|
314
|
-
}
|
|
315
|
-
getKey($ref) {
|
|
316
|
-
const key = $ref.split("/").pop();
|
|
317
|
-
return key === "" ? void 0 : key;
|
|
318
|
-
}
|
|
319
|
-
set($ref, value) {
|
|
320
|
-
$ref = $ref.trim();
|
|
321
|
-
if ($ref === "") return false;
|
|
322
|
-
if ($ref.startsWith("#")) {
|
|
323
|
-
$ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
324
|
-
jsonpointer.set(this.api, $ref, value);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
#setDiscriminator(schema) {
|
|
328
|
-
const { mapping = {}, propertyName } = schema.discriminator;
|
|
329
|
-
if (this.#options.discriminator === "inherit") Object.entries(mapping).forEach(([mappingKey, mappingValue]) => {
|
|
330
|
-
if (mappingValue) {
|
|
331
|
-
const childSchema = this.get(mappingValue);
|
|
332
|
-
if (!childSchema) return;
|
|
333
|
-
if (!childSchema.properties) childSchema.properties = {};
|
|
334
|
-
const property = childSchema.properties[propertyName];
|
|
335
|
-
if (childSchema.properties) {
|
|
336
|
-
childSchema.properties[propertyName] = {
|
|
337
|
-
...childSchema.properties ? childSchema.properties[propertyName] : {},
|
|
338
|
-
enum: [...property?.enum?.filter((value) => value !== mappingKey) ?? [], mappingKey]
|
|
339
|
-
};
|
|
340
|
-
childSchema.required = typeof childSchema.required === "boolean" ? childSchema.required : [...new Set([...childSchema.required ?? [], propertyName])];
|
|
341
|
-
this.set(mappingValue, childSchema);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
getDiscriminator(schema) {
|
|
347
|
-
if (!isDiscriminator(schema) || !schema) return null;
|
|
348
|
-
const { mapping = {}, propertyName } = schema.discriminator;
|
|
349
|
-
/**
|
|
350
|
-
* Helper to extract discriminator value from a schema.
|
|
351
|
-
* Checks in order:
|
|
352
|
-
* 1. Extension property matching propertyName (e.g., x-linode-ref-name)
|
|
353
|
-
* 2. Property with const value
|
|
354
|
-
* 3. Property with single enum value
|
|
355
|
-
* 4. Title as fallback
|
|
356
|
-
*/
|
|
357
|
-
const getDiscriminatorValue = (schema) => {
|
|
358
|
-
if (!schema) return null;
|
|
359
|
-
if (propertyName.startsWith("x-")) {
|
|
360
|
-
const extensionValue = schema[propertyName];
|
|
361
|
-
if (extensionValue && typeof extensionValue === "string") return extensionValue;
|
|
362
|
-
}
|
|
363
|
-
const propertySchema = schema.properties?.[propertyName];
|
|
364
|
-
if (propertySchema && "const" in propertySchema && propertySchema.const !== void 0) return String(propertySchema.const);
|
|
365
|
-
if (propertySchema && propertySchema.enum?.length === 1) return String(propertySchema.enum[0]);
|
|
366
|
-
return schema.title || null;
|
|
367
|
-
};
|
|
368
|
-
/**
|
|
369
|
-
* Process oneOf/anyOf items to build mapping.
|
|
370
|
-
* Handles both $ref and inline schemas.
|
|
371
|
-
*/
|
|
372
|
-
const processSchemas = (schemas, existingMapping) => {
|
|
373
|
-
schemas.forEach((schemaItem, index) => {
|
|
374
|
-
if (isReference(schemaItem)) {
|
|
375
|
-
const key = this.getKey(schemaItem.$ref);
|
|
376
|
-
try {
|
|
377
|
-
const discriminatorValue = getDiscriminatorValue(this.get(schemaItem.$ref));
|
|
378
|
-
const canAdd = key && !Object.values(existingMapping).includes(schemaItem.$ref);
|
|
379
|
-
if (canAdd && discriminatorValue) existingMapping[discriminatorValue] = schemaItem.$ref;
|
|
380
|
-
else if (canAdd) existingMapping[key] = schemaItem.$ref;
|
|
381
|
-
} catch (_error) {
|
|
382
|
-
if (key && !Object.values(existingMapping).includes(schemaItem.$ref)) existingMapping[key] = schemaItem.$ref;
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
const discriminatorValue = getDiscriminatorValue(schemaItem);
|
|
386
|
-
if (discriminatorValue) existingMapping[discriminatorValue] = `${KUBB_INLINE_REF_PREFIX}${index}`;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
};
|
|
390
|
-
if (schema.oneOf) processSchemas(schema.oneOf, mapping);
|
|
391
|
-
if (schema.anyOf) processSchemas(schema.anyOf, mapping);
|
|
392
|
-
return {
|
|
393
|
-
...schema.discriminator,
|
|
394
|
-
mapping
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
dereferenceWithRef(schema) {
|
|
398
|
-
if (isReference(schema)) return {
|
|
399
|
-
...schema,
|
|
400
|
-
...this.get(schema.$ref),
|
|
401
|
-
$ref: schema.$ref
|
|
402
|
-
};
|
|
403
|
-
return schema;
|
|
404
|
-
}
|
|
405
|
-
#applyDiscriminatorInheritance() {
|
|
406
|
-
const components = this.api.components;
|
|
407
|
-
if (!components?.schemas) return;
|
|
408
|
-
const visited = /* @__PURE__ */ new WeakSet();
|
|
409
|
-
const enqueue = (value) => {
|
|
410
|
-
if (!value) return;
|
|
411
|
-
if (Array.isArray(value)) {
|
|
412
|
-
for (const item of value) enqueue(item);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (typeof value === "object") visit(value);
|
|
416
|
-
};
|
|
417
|
-
const visit = (schema) => {
|
|
418
|
-
if (!schema || typeof schema !== "object") return;
|
|
419
|
-
if (isReference(schema)) {
|
|
420
|
-
visit(this.get(schema.$ref));
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const schemaObject = schema;
|
|
424
|
-
if (visited.has(schemaObject)) return;
|
|
425
|
-
visited.add(schemaObject);
|
|
426
|
-
if (isDiscriminator(schemaObject)) this.#setDiscriminator(schemaObject);
|
|
427
|
-
if ("allOf" in schemaObject) enqueue(schemaObject.allOf);
|
|
428
|
-
if ("oneOf" in schemaObject) enqueue(schemaObject.oneOf);
|
|
429
|
-
if ("anyOf" in schemaObject) enqueue(schemaObject.anyOf);
|
|
430
|
-
if ("not" in schemaObject) enqueue(schemaObject.not);
|
|
431
|
-
if ("items" in schemaObject) enqueue(schemaObject.items);
|
|
432
|
-
if ("prefixItems" in schemaObject) enqueue(schemaObject.prefixItems);
|
|
433
|
-
if (schemaObject.properties) enqueue(Object.values(schemaObject.properties));
|
|
434
|
-
if (schemaObject.additionalProperties && typeof schemaObject.additionalProperties === "object") enqueue(schemaObject.additionalProperties);
|
|
435
|
-
};
|
|
436
|
-
for (const schema of Object.values(components.schemas)) visit(schema);
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Oas does not have a getResponseBody(contentType)
|
|
440
|
-
*/
|
|
441
|
-
#getResponseBodyFactory(responseBody) {
|
|
442
|
-
function hasResponseBody(res = responseBody) {
|
|
443
|
-
return !!res;
|
|
444
|
-
}
|
|
445
|
-
return (contentType) => {
|
|
446
|
-
if (!hasResponseBody(responseBody)) return false;
|
|
447
|
-
if (isReference(responseBody)) return false;
|
|
448
|
-
if (!responseBody.content) return false;
|
|
449
|
-
if (contentType) {
|
|
450
|
-
if (!(contentType in responseBody.content)) return false;
|
|
451
|
-
return responseBody.content[contentType];
|
|
452
|
-
}
|
|
453
|
-
let availableContentType;
|
|
454
|
-
const contentTypes = Object.keys(responseBody.content);
|
|
455
|
-
contentTypes.forEach((mt) => {
|
|
456
|
-
if (!availableContentType && matchesMimeType.json(mt)) availableContentType = mt;
|
|
457
|
-
});
|
|
458
|
-
if (!availableContentType) contentTypes.forEach((mt) => {
|
|
459
|
-
if (!availableContentType) availableContentType = mt;
|
|
460
|
-
});
|
|
461
|
-
if (availableContentType) return [
|
|
462
|
-
availableContentType,
|
|
463
|
-
responseBody.content[availableContentType],
|
|
464
|
-
...responseBody.description ? [responseBody.description] : []
|
|
465
|
-
];
|
|
466
|
-
return false;
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
getResponseSchema(operation, statusCode) {
|
|
470
|
-
if (operation.schema.responses) Object.keys(operation.schema.responses).forEach((key) => {
|
|
471
|
-
const schema = operation.schema.responses[key];
|
|
472
|
-
const $ref = isReference(schema) ? schema.$ref : void 0;
|
|
473
|
-
if (schema && $ref) operation.schema.responses[key] = this.get($ref);
|
|
474
|
-
});
|
|
475
|
-
const getResponseBody = this.#getResponseBodyFactory(operation.getResponseByStatusCode(statusCode));
|
|
476
|
-
const { contentType } = this.#options;
|
|
477
|
-
const responseBody = getResponseBody(contentType);
|
|
478
|
-
if (responseBody === false) return {};
|
|
479
|
-
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
|
|
480
|
-
if (!schema) return {};
|
|
481
|
-
return this.dereferenceWithRef(schema);
|
|
482
|
-
}
|
|
483
|
-
getRequestSchema(operation) {
|
|
484
|
-
const { contentType } = this.#options;
|
|
485
|
-
if (operation.schema.requestBody) operation.schema.requestBody = this.dereferenceWithRef(operation.schema.requestBody);
|
|
486
|
-
const requestBody = operation.getRequestBody(contentType);
|
|
487
|
-
if (requestBody === false) return;
|
|
488
|
-
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
489
|
-
if (!schema) return;
|
|
490
|
-
return this.dereferenceWithRef(schema);
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Returns all resolved parameters for an operation, merging path-level and operation-level
|
|
494
|
-
* parameters and deduplicating by `in:name` (operation-level takes precedence).
|
|
414
|
+
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
495
415
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
|
|
500
|
-
getParameters(operation) {
|
|
501
|
-
const resolveParams = (params) => params.map((p) => this.dereferenceWithRef(p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
|
|
502
|
-
const operationParams = resolveParams(operation.schema?.parameters || []);
|
|
503
|
-
const pathItem = this.api?.paths?.[operation.path];
|
|
504
|
-
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
|
|
505
|
-
const paramMap = /* @__PURE__ */ new Map();
|
|
506
|
-
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
507
|
-
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
508
|
-
return Array.from(paramMap.values());
|
|
509
|
-
}
|
|
510
|
-
getParametersSchema(operation, inKey) {
|
|
511
|
-
const { contentType = operation.getContentType() } = this.#options;
|
|
512
|
-
const params = this.getParameters(operation).filter((v) => v.in === inKey);
|
|
513
|
-
if (!params.length) return null;
|
|
514
|
-
return params.reduce((schema, pathParameters) => {
|
|
515
|
-
const property = pathParameters.content?.[contentType]?.schema ?? pathParameters.schema;
|
|
516
|
-
const required = typeof schema.required === "boolean" ? schema.required : [...schema.required || [], pathParameters.required ? pathParameters.name : void 0].filter(Boolean);
|
|
517
|
-
const getDefaultStyle = (location) => {
|
|
518
|
-
if (location === "query") return "form";
|
|
519
|
-
if (location === "path") return "simple";
|
|
520
|
-
return "simple";
|
|
521
|
-
};
|
|
522
|
-
const style = pathParameters.style || getDefaultStyle(inKey);
|
|
523
|
-
const explode = pathParameters.explode !== void 0 ? pathParameters.explode : style === "form";
|
|
524
|
-
if (inKey === "query" && style === "form" && explode === true && property?.type === "object" && property?.additionalProperties && !property?.properties) return {
|
|
525
|
-
...schema,
|
|
526
|
-
description: pathParameters.description || schema.description,
|
|
527
|
-
deprecated: schema.deprecated,
|
|
528
|
-
example: property.example || schema.example,
|
|
529
|
-
additionalProperties: property.additionalProperties
|
|
530
|
-
};
|
|
531
|
-
return {
|
|
532
|
-
...schema,
|
|
533
|
-
description: schema.description,
|
|
534
|
-
deprecated: schema.deprecated,
|
|
535
|
-
example: schema.example,
|
|
536
|
-
required,
|
|
537
|
-
properties: {
|
|
538
|
-
...schema.properties,
|
|
539
|
-
[pathParameters.name]: {
|
|
540
|
-
description: pathParameters.description,
|
|
541
|
-
...property
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
}, {
|
|
546
|
-
type: "object",
|
|
547
|
-
required: [],
|
|
548
|
-
properties: {}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
async validate() {
|
|
552
|
-
return validate(this.api);
|
|
553
|
-
}
|
|
554
|
-
flattenSchema(schema) {
|
|
555
|
-
return flattenSchema(schema);
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Get schemas from OpenAPI components (schemas, responses, requestBodies).
|
|
559
|
-
* Returns schemas in dependency order along with name mapping for collision resolution.
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
|
|
419
|
+
* ```
|
|
560
420
|
*/
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const includes = options.includes ?? [
|
|
564
|
-
"schemas",
|
|
565
|
-
"requestBodies",
|
|
566
|
-
"responses"
|
|
567
|
-
];
|
|
568
|
-
const components = this.getDefinition().components;
|
|
569
|
-
const schemasWithMeta = [];
|
|
570
|
-
if (includes.includes("schemas")) {
|
|
571
|
-
const componentSchemas = components?.schemas || {};
|
|
572
|
-
for (const [name, schemaObject] of Object.entries(componentSchemas)) {
|
|
573
|
-
let schema = schemaObject;
|
|
574
|
-
if (isReference(schemaObject)) {
|
|
575
|
-
const resolved = this.get(schemaObject.$ref);
|
|
576
|
-
if (resolved && !isReference(resolved)) schema = resolved;
|
|
577
|
-
}
|
|
578
|
-
schemasWithMeta.push({
|
|
579
|
-
schema,
|
|
580
|
-
source: "schemas",
|
|
581
|
-
originalName: name
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (includes.includes("responses")) {
|
|
586
|
-
const responses = components?.responses || {};
|
|
587
|
-
for (const [name, response] of Object.entries(responses)) {
|
|
588
|
-
const schema = extractSchemaFromContent(response.content, contentType);
|
|
589
|
-
if (schema) {
|
|
590
|
-
let resolvedSchema = schema;
|
|
591
|
-
if (isReference(schema)) {
|
|
592
|
-
const resolved = this.get(schema.$ref);
|
|
593
|
-
if (resolved && !isReference(resolved)) resolvedSchema = resolved;
|
|
594
|
-
}
|
|
595
|
-
schemasWithMeta.push({
|
|
596
|
-
schema: resolvedSchema,
|
|
597
|
-
source: "responses",
|
|
598
|
-
originalName: name
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (includes.includes("requestBodies")) {
|
|
604
|
-
const requestBodies = components?.requestBodies || {};
|
|
605
|
-
for (const [name, request] of Object.entries(requestBodies)) {
|
|
606
|
-
const schema = extractSchemaFromContent(request.content, contentType);
|
|
607
|
-
if (schema) {
|
|
608
|
-
let resolvedSchema = schema;
|
|
609
|
-
if (isReference(schema)) {
|
|
610
|
-
const resolved = this.get(schema.$ref);
|
|
611
|
-
if (resolved && !isReference(resolved)) resolvedSchema = resolved;
|
|
612
|
-
}
|
|
613
|
-
schemasWithMeta.push({
|
|
614
|
-
schema: resolvedSchema,
|
|
615
|
-
source: "requestBodies",
|
|
616
|
-
originalName: name
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const { schemas, nameMapping } = resolveCollisions(schemasWithMeta);
|
|
622
|
-
return {
|
|
623
|
-
schemas: sortSchemas(schemas),
|
|
624
|
-
nameMapping
|
|
625
|
-
};
|
|
421
|
+
toURLPath() {
|
|
422
|
+
return this.path.replace(/\{([^}]+)\}/g, ":$1");
|
|
626
423
|
}
|
|
627
424
|
};
|
|
628
425
|
//#endregion
|
|
629
|
-
//#region src/
|
|
426
|
+
//#region src/guards.ts
|
|
630
427
|
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
428
|
+
* Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* if (isOpenApiV2Document(doc)) {
|
|
433
|
+
* // doc is OpenAPIV2.Document
|
|
434
|
+
* }
|
|
435
|
+
* ```
|
|
633
436
|
*/
|
|
634
437
|
function isOpenApiV2Document(doc) {
|
|
635
438
|
return !!doc && isPlainObject(doc) && !("openapi" in doc);
|
|
@@ -637,9 +440,15 @@ function isOpenApiV2Document(doc) {
|
|
|
637
440
|
/**
|
|
638
441
|
* Returns `true` when a schema should be treated as nullable.
|
|
639
442
|
*
|
|
640
|
-
*
|
|
641
|
-
* -
|
|
642
|
-
*
|
|
443
|
+
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
444
|
+
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```ts
|
|
448
|
+
* isNullable({ type: 'string', nullable: true }) // true
|
|
449
|
+
* isNullable({ type: ['string', 'null'] }) // true
|
|
450
|
+
* isNullable({ type: 'string' }) // false
|
|
451
|
+
* ```
|
|
643
452
|
*/
|
|
644
453
|
function isNullable(schema) {
|
|
645
454
|
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
@@ -649,34 +458,51 @@ function isNullable(schema) {
|
|
|
649
458
|
return false;
|
|
650
459
|
}
|
|
651
460
|
/**
|
|
652
|
-
*
|
|
653
|
-
*
|
|
461
|
+
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* isReference({ $ref: '#/components/schemas/Pet' }) // true
|
|
466
|
+
* isReference({ type: 'string' }) // false
|
|
467
|
+
* ```
|
|
654
468
|
*/
|
|
655
469
|
function isReference(obj) {
|
|
656
|
-
return !!obj &&
|
|
470
|
+
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
657
471
|
}
|
|
658
472
|
/**
|
|
659
|
-
* Returns `true` when `obj` is a schema
|
|
660
|
-
*
|
|
473
|
+
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
|
|
478
|
+
* isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
|
|
479
|
+
* ```
|
|
661
480
|
*/
|
|
662
481
|
function isDiscriminator(obj) {
|
|
663
482
|
const record = obj;
|
|
664
483
|
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
665
484
|
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/factory.ts
|
|
666
487
|
/**
|
|
667
|
-
* Loads
|
|
488
|
+
* Loads and dereferences an OpenAPI document, returning the raw `Document`.
|
|
668
489
|
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
490
|
+
* Accepts a file path string or an already-parsed document object. File paths are bundled via
|
|
491
|
+
* Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
|
|
492
|
+
* to OpenAPI 3.0 via `swagger2openapi`.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```ts
|
|
496
|
+
* const document = await parseDocument('./openapi.yaml')
|
|
497
|
+
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
498
|
+
* ```
|
|
672
499
|
*/
|
|
673
|
-
async function
|
|
674
|
-
if (typeof pathOrApi === "string" && canBundle) return
|
|
500
|
+
async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true } = {}) {
|
|
501
|
+
if (typeof pathOrApi === "string" && canBundle) return parseDocument((await bundle({
|
|
675
502
|
ref: pathOrApi,
|
|
676
503
|
config: await loadConfig(),
|
|
677
504
|
base: pathOrApi
|
|
678
505
|
})).bundle.parsed, {
|
|
679
|
-
oasClass,
|
|
680
506
|
canBundle,
|
|
681
507
|
enablePaths
|
|
682
508
|
});
|
|
@@ -686,26 +512,27 @@ async function parse(pathOrApi, { oasClass = Oas, canBundle = true, enablePaths
|
|
|
686
512
|
}).load();
|
|
687
513
|
if (isOpenApiV2Document(document)) {
|
|
688
514
|
const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
|
|
689
|
-
return
|
|
515
|
+
return openapi;
|
|
690
516
|
}
|
|
691
|
-
return
|
|
517
|
+
return document;
|
|
692
518
|
}
|
|
693
519
|
/**
|
|
694
|
-
* Deep-merges multiple OpenAPI documents into a single `
|
|
520
|
+
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
695
521
|
*
|
|
696
|
-
* Each document is parsed independently
|
|
697
|
-
*
|
|
698
|
-
* the returned `Oas` instance is fully initialized.
|
|
522
|
+
* Each document is parsed independently then recursively merged with `remeda`'s `mergeDeep`.
|
|
523
|
+
* Throws when the input array is empty.
|
|
699
524
|
*
|
|
700
|
-
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
|
|
528
|
+
* ```
|
|
701
529
|
*/
|
|
702
|
-
async function
|
|
703
|
-
const
|
|
704
|
-
oasClass,
|
|
530
|
+
async function mergeDocuments(pathOrApi) {
|
|
531
|
+
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, {
|
|
705
532
|
enablePaths: false,
|
|
706
533
|
canBundle: false
|
|
707
534
|
})));
|
|
708
|
-
if (
|
|
535
|
+
if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
|
|
709
536
|
const seed = {
|
|
710
537
|
openapi: MERGE_OPENAPI_VERSION,
|
|
711
538
|
info: {
|
|
@@ -715,38 +542,248 @@ async function merge(pathOrApi, { oasClass = Oas } = {}) {
|
|
|
715
542
|
paths: {},
|
|
716
543
|
components: { schemas: {} }
|
|
717
544
|
};
|
|
718
|
-
return
|
|
545
|
+
return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
|
|
719
546
|
}
|
|
720
547
|
/**
|
|
721
|
-
*
|
|
548
|
+
* Creates a `Document` from an `AdapterSource`.
|
|
549
|
+
*
|
|
550
|
+
* Handles all three source types:
|
|
551
|
+
* - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
|
|
552
|
+
* - `{ type: 'paths' }` — merges multiple file paths into a single document.
|
|
553
|
+
* - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
|
|
722
554
|
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```ts
|
|
557
|
+
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
558
|
+
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
559
|
+
* ```
|
|
727
560
|
*/
|
|
728
|
-
function parseFromConfig(
|
|
729
|
-
if ("data"
|
|
730
|
-
if (typeof
|
|
561
|
+
function parseFromConfig(source) {
|
|
562
|
+
if (source.type === "data") {
|
|
563
|
+
if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
|
|
731
564
|
try {
|
|
732
|
-
return
|
|
565
|
+
return parseDocument(yaml.parse(source.data));
|
|
733
566
|
} catch {
|
|
734
|
-
return
|
|
567
|
+
return parseDocument(source.data);
|
|
735
568
|
}
|
|
736
569
|
}
|
|
737
|
-
if (
|
|
738
|
-
if (new URLPath(
|
|
739
|
-
return
|
|
570
|
+
if (source.type === "paths") return mergeDocuments(source.paths);
|
|
571
|
+
if (new URLPath(source.path).isURL) return parseDocument(source.path);
|
|
572
|
+
return parseDocument(path.resolve(path.dirname(source.path), source.path));
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Validates an OpenAPI document using `oas-normalize` with colorized error output.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* await validateDocument(document)
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
async function validateDocument(document) {
|
|
583
|
+
try {
|
|
584
|
+
await new OASNormalize(document, {
|
|
585
|
+
enablePaths: true,
|
|
586
|
+
colorizeErrors: true
|
|
587
|
+
}).validate({ parser: { validate: { errors: { colorize: true } } } });
|
|
588
|
+
} catch (_err) {}
|
|
589
|
+
}
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/refs.ts
|
|
592
|
+
/**
|
|
593
|
+
* Resolves a local JSON pointer reference from a document.
|
|
594
|
+
*
|
|
595
|
+
* Accepts `#/...` refs. Returns `null` for empty or non-local refs.
|
|
596
|
+
* Throws when the pointer cannot be resolved.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
function resolveRef(document, $ref) {
|
|
604
|
+
const origRef = $ref;
|
|
605
|
+
$ref = $ref.trim();
|
|
606
|
+
if ($ref === "") return null;
|
|
607
|
+
if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
608
|
+
else return null;
|
|
609
|
+
const current = jsonpointer.get(document, $ref);
|
|
610
|
+
if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
|
|
611
|
+
return current;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
615
|
+
*
|
|
616
|
+
* Useful for parser flows that need both dereferenced fields and pointer
|
|
617
|
+
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
618
|
+
*
|
|
619
|
+
* @example
|
|
620
|
+
* ```ts
|
|
621
|
+
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
622
|
+
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
function dereferenceWithRef(document, schema) {
|
|
626
|
+
if (isReference(schema)) return {
|
|
627
|
+
...schema,
|
|
628
|
+
...resolveRef(document, schema.$ref),
|
|
629
|
+
$ref: schema.$ref
|
|
630
|
+
};
|
|
631
|
+
return schema;
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/resolvers.ts
|
|
635
|
+
/**
|
|
636
|
+
* Resolves `{variable}` placeholders in an OpenAPI server URL.
|
|
637
|
+
*
|
|
638
|
+
* Resolution order per variable: `overrides[key]` → `variable.default` → left unreplaced.
|
|
639
|
+
* Throws when an override value is not in the variable's allowed `enum` list.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* resolveServerUrl(
|
|
644
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
645
|
+
* { env: 'prod' },
|
|
646
|
+
* )
|
|
647
|
+
* // 'https://prod.api.example.com'
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
function resolveServerUrl(server, overrides) {
|
|
651
|
+
if (!server.variables) return server.url;
|
|
652
|
+
let url = server.url;
|
|
653
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
654
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
655
|
+
if (value === void 0) continue;
|
|
656
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
|
|
657
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
658
|
+
}
|
|
659
|
+
return url;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Looks up the Kubb `SchemaType` for a given OAS `format` string.
|
|
663
|
+
* Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
|
|
664
|
+
* which are handled separately because their output depends on parser options.
|
|
665
|
+
*/
|
|
666
|
+
function getSchemaType(format) {
|
|
667
|
+
return formatMap[format] ?? null;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
671
|
+
* Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
|
|
672
|
+
* `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
|
|
673
|
+
*/
|
|
674
|
+
function getPrimitiveType(type) {
|
|
675
|
+
if (type === "number" || type === "integer" || type === "bigint") return type;
|
|
676
|
+
if (type === "boolean") return "boolean";
|
|
677
|
+
return "string";
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
|
|
681
|
+
* Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
|
|
682
|
+
*/
|
|
683
|
+
function getMediaType(contentType) {
|
|
684
|
+
return Object.values(mediaTypes).includes(contentType) ? contentType : null;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Returns all resolved parameters for an operation, merging path-level and operation-level entries.
|
|
688
|
+
*
|
|
689
|
+
* Operation-level parameters take precedence over path-level ones with the same `in:name` key.
|
|
690
|
+
* `$ref` parameters are resolved via `dereferenceWithRef` to restore backward compatibility
|
|
691
|
+
* with `oas` v31+ which otherwise filters them out.
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```ts
|
|
695
|
+
* getParameters(document, operation)
|
|
696
|
+
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
function getParameters(document, operation) {
|
|
700
|
+
const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
|
|
701
|
+
const operationParams = resolveParams(operation.schema?.parameters || []);
|
|
702
|
+
const pathItem = document.paths?.[operation.path];
|
|
703
|
+
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : []);
|
|
704
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
705
|
+
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
706
|
+
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
707
|
+
return Array.from(paramMap.values());
|
|
708
|
+
}
|
|
709
|
+
function getResponseBody(responseBody, contentType) {
|
|
710
|
+
if (!responseBody) return false;
|
|
711
|
+
if (isReference(responseBody)) return false;
|
|
712
|
+
const body = responseBody;
|
|
713
|
+
if (!body.content) return false;
|
|
714
|
+
if (contentType) {
|
|
715
|
+
if (!(contentType in body.content)) return false;
|
|
716
|
+
return body.content[contentType];
|
|
717
|
+
}
|
|
718
|
+
let availableContentType;
|
|
719
|
+
const contentTypes = Object.keys(body.content);
|
|
720
|
+
contentTypes.forEach((mt) => {
|
|
721
|
+
if (!availableContentType && matchesMimeType.json(mt)) availableContentType = mt;
|
|
722
|
+
});
|
|
723
|
+
if (!availableContentType) contentTypes.forEach((mt) => {
|
|
724
|
+
if (!availableContentType) availableContentType = mt;
|
|
725
|
+
});
|
|
726
|
+
if (availableContentType) return [
|
|
727
|
+
availableContentType,
|
|
728
|
+
body.content[availableContentType],
|
|
729
|
+
...body.description ? [body.description] : []
|
|
730
|
+
];
|
|
731
|
+
return false;
|
|
740
732
|
}
|
|
741
733
|
/**
|
|
742
|
-
*
|
|
734
|
+
* Returns the response schema for a given operation and HTTP status code.
|
|
735
|
+
*
|
|
736
|
+
* Returns an empty object `{}` when no response body schema is available.
|
|
743
737
|
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```ts
|
|
740
|
+
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
741
|
+
* getResponseSchema(document, operation, '4XX') // {}
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
745
|
+
if (operation.schema.responses) Object.keys(operation.schema.responses).forEach((key) => {
|
|
746
|
+
const schema = operation.schema.responses[key];
|
|
747
|
+
const $ref = isReference(schema) ? schema.$ref : void 0;
|
|
748
|
+
if (schema && $ref) operation.schema.responses[key] = resolveRef(document, $ref);
|
|
749
|
+
});
|
|
750
|
+
const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
|
|
751
|
+
if (responseBody === false) return {};
|
|
752
|
+
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
|
|
753
|
+
if (!schema) return {};
|
|
754
|
+
return dereferenceWithRef(document, schema);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Returns the request body schema for an operation, or `undefined` when absent.
|
|
747
758
|
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
759
|
+
* @example
|
|
760
|
+
* ```ts
|
|
761
|
+
* getRequestSchema(document, operation) // SchemaObject | null
|
|
762
|
+
* ```
|
|
763
|
+
*/
|
|
764
|
+
function getRequestSchema(document, operation, options = {}) {
|
|
765
|
+
if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
|
|
766
|
+
const requestBody = operation.getRequestBody(options.contentType);
|
|
767
|
+
if (requestBody === false) return null;
|
|
768
|
+
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
769
|
+
if (!schema) return null;
|
|
770
|
+
return dereferenceWithRef(document, schema);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Flattens a keyword-only `allOf` into its parent schema.
|
|
774
|
+
*
|
|
775
|
+
* Only flattens when every member is a plain fragment — no `$ref` and no structural keywords
|
|
776
|
+
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
777
|
+
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* ```ts
|
|
781
|
+
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
782
|
+
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
783
|
+
*
|
|
784
|
+
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
785
|
+
* // returned unchanged — contains a $ref
|
|
786
|
+
* ```
|
|
750
787
|
*/
|
|
751
788
|
function flattenSchema(schema) {
|
|
752
789
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
|
|
@@ -759,18 +796,26 @@ function flattenSchema(schema) {
|
|
|
759
796
|
return merged;
|
|
760
797
|
}
|
|
761
798
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
799
|
+
* Extracts the inline schema from a media-type `content` map.
|
|
800
|
+
*
|
|
801
|
+
* Prefers `preferredContentType` when given; otherwise uses the first key in the map.
|
|
802
|
+
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```ts
|
|
806
|
+
* extractSchemaFromContent(operation.content, 'application/json')
|
|
807
|
+
* // SchemaObject | null
|
|
808
|
+
* ```
|
|
764
809
|
*/
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
810
|
+
function extractSchemaFromContent(content, preferredContentType) {
|
|
811
|
+
if (!content) return null;
|
|
812
|
+
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
813
|
+
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
814
|
+
if (schema && "$ref" in schema) return null;
|
|
815
|
+
return schema ?? null;
|
|
770
816
|
}
|
|
771
817
|
/**
|
|
772
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>`
|
|
773
|
-
* Used by `sortSchemas` to build the dependency graph.
|
|
818
|
+
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
774
819
|
*/
|
|
775
820
|
function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
|
|
776
821
|
if (Array.isArray(schema)) {
|
|
@@ -786,11 +831,14 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
|
|
|
786
831
|
/**
|
|
787
832
|
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
788
833
|
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* referenced types before the types that depend on them.
|
|
834
|
+
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
835
|
+
* can emit types in the correct order. Cycles are silently skipped.
|
|
792
836
|
*
|
|
793
|
-
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```ts
|
|
839
|
+
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
840
|
+
* // Pet appears before Order when Order.$ref points at Pet
|
|
841
|
+
* ```
|
|
794
842
|
*/
|
|
795
843
|
function sortSchemas(schemas) {
|
|
796
844
|
const deps = /* @__PURE__ */ new Map();
|
|
@@ -810,26 +858,6 @@ function sortSchemas(schemas) {
|
|
|
810
858
|
for (const name of sorted) result[name] = schemas[name];
|
|
811
859
|
return result;
|
|
812
860
|
}
|
|
813
|
-
/**
|
|
814
|
-
* Extracts the inline schema from a media-type `content` map.
|
|
815
|
-
*
|
|
816
|
-
* Prefers `preferredContentType` when provided; otherwise falls back to the
|
|
817
|
-
* first key in the map. Returns `null` when:
|
|
818
|
-
* - `content` is absent.
|
|
819
|
-
* - The target content-type has no `schema` entry.
|
|
820
|
-
* - The schema is a `$ref` (callers resolve refs separately).
|
|
821
|
-
*/
|
|
822
|
-
function extractSchemaFromContent(content, preferredContentType) {
|
|
823
|
-
if (!content) return null;
|
|
824
|
-
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
825
|
-
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
826
|
-
if (schema && "$ref" in schema) return null;
|
|
827
|
-
return schema ?? null;
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* Returns the PascalCase suffix appended to a component name when resolving
|
|
831
|
-
* cross-source name collisions (schemas vs. responses vs. requestBodies).
|
|
832
|
-
*/
|
|
833
861
|
const semanticSuffixes = {
|
|
834
862
|
schemas: "Schema",
|
|
835
863
|
responses: "Response",
|
|
@@ -838,261 +866,138 @@ const semanticSuffixes = {
|
|
|
838
866
|
function getSemanticSuffix(source) {
|
|
839
867
|
return semanticSuffixes[source];
|
|
840
868
|
}
|
|
869
|
+
function resolveSchemaRef(document, schema) {
|
|
870
|
+
if (!isReference(schema)) return schema;
|
|
871
|
+
const resolved = resolveRef(document, schema.$ref);
|
|
872
|
+
return resolved && !isReference(resolved) ? resolved : schema;
|
|
873
|
+
}
|
|
841
874
|
/**
|
|
842
|
-
*
|
|
875
|
+
* Collects component schemas from one or more sources and resolves name collisions.
|
|
876
|
+
*
|
|
877
|
+
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
878
|
+
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
843
879
|
*
|
|
844
880
|
* When two or more schemas normalize to the same PascalCase name:
|
|
845
|
-
* -
|
|
846
|
-
* -
|
|
847
|
-
* a semantic suffix (`Schema`, `Response`, `Request`) is appended.
|
|
881
|
+
* - Same source → numeric suffix (`2`, `3`, …).
|
|
882
|
+
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
848
883
|
*
|
|
849
|
-
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```ts
|
|
886
|
+
* const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
|
|
887
|
+
* ```
|
|
850
888
|
*/
|
|
851
|
-
function
|
|
852
|
-
const
|
|
853
|
-
const
|
|
889
|
+
function getSchemas(document, { contentType }) {
|
|
890
|
+
const components = document.components;
|
|
891
|
+
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
892
|
+
schema: resolveSchemaRef(document, schema),
|
|
893
|
+
source: "schemas",
|
|
894
|
+
originalName: name
|
|
895
|
+
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
896
|
+
const schema = extractSchemaFromContent(item.content, contentType);
|
|
897
|
+
return schema ? [{
|
|
898
|
+
schema: resolveSchemaRef(document, schema),
|
|
899
|
+
source,
|
|
900
|
+
originalName: name
|
|
901
|
+
}] : [];
|
|
902
|
+
}))];
|
|
854
903
|
const normalizedNames = /* @__PURE__ */ new Map();
|
|
855
|
-
for (const item of
|
|
856
|
-
const
|
|
857
|
-
const bucket = normalizedNames.get(
|
|
904
|
+
for (const item of candidates) {
|
|
905
|
+
const key = pascalCase(item.originalName);
|
|
906
|
+
const bucket = normalizedNames.get(key) ?? [];
|
|
858
907
|
bucket.push(item);
|
|
859
|
-
normalizedNames.set(
|
|
860
|
-
}
|
|
861
|
-
for (const [, items] of normalizedNames) {
|
|
862
|
-
if (items.length === 1) {
|
|
863
|
-
const item = items[0];
|
|
864
|
-
schemas[item.originalName] = item.schema;
|
|
865
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName);
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
if (new Set(items.map((item) => item.source)).size === 1) items.forEach((item, index) => {
|
|
869
|
-
const uniqueName = item.originalName + (index === 0 ? "" : (index + 1).toString());
|
|
870
|
-
schemas[uniqueName] = item.schema;
|
|
871
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
872
|
-
});
|
|
873
|
-
else items.forEach((item) => {
|
|
874
|
-
const uniqueName = item.originalName + getSemanticSuffix(item.source);
|
|
875
|
-
schemas[uniqueName] = item.schema;
|
|
876
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
877
|
-
});
|
|
908
|
+
normalizedNames.set(key, bucket);
|
|
878
909
|
}
|
|
910
|
+
const schemas = {};
|
|
911
|
+
const nameMapping = /* @__PURE__ */ new Map();
|
|
912
|
+
const multipleSources = (items) => new Set(items.map((i) => i.source)).size > 1;
|
|
913
|
+
for (const [, items] of normalizedNames) items.forEach((item, index) => {
|
|
914
|
+
const suffix = items.length === 1 ? "" : multipleSources(items) ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
|
|
915
|
+
const uniqueName = item.originalName + suffix;
|
|
916
|
+
schemas[uniqueName] = item.schema;
|
|
917
|
+
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
918
|
+
});
|
|
879
919
|
return {
|
|
880
|
-
schemas,
|
|
920
|
+
schemas: sortSchemas(schemas),
|
|
881
921
|
nameMapping
|
|
882
922
|
};
|
|
883
923
|
}
|
|
884
|
-
//#endregion
|
|
885
|
-
//#region src/discriminator.ts
|
|
886
|
-
function resolveDiscriminatorValue(mapping, ref) {
|
|
887
|
-
if (!mapping || !ref) return void 0;
|
|
888
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0];
|
|
889
|
-
}
|
|
890
|
-
function createDiscriminantNode(propertyName, value) {
|
|
891
|
-
return createSchema({
|
|
892
|
-
type: "object",
|
|
893
|
-
primitive: "object",
|
|
894
|
-
properties: [createProperty({
|
|
895
|
-
name: propertyName,
|
|
896
|
-
schema: createSchema({
|
|
897
|
-
type: "enum",
|
|
898
|
-
primitive: "string",
|
|
899
|
-
enumValues: [value]
|
|
900
|
-
}),
|
|
901
|
-
required: true
|
|
902
|
-
})]
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
//#endregion
|
|
906
|
-
//#region src/naming.ts
|
|
907
|
-
function resolveChildName(parentName, propName) {
|
|
908
|
-
return parentName ? pascalCase([parentName, propName].join(" ")) : void 0;
|
|
909
|
-
}
|
|
910
|
-
function resolveEnumPropName(parentName, propName, enumSuffix) {
|
|
911
|
-
return pascalCase([
|
|
912
|
-
parentName,
|
|
913
|
-
propName,
|
|
914
|
-
enumSuffix
|
|
915
|
-
].filter(Boolean).join(" "));
|
|
916
|
-
}
|
|
917
|
-
function applyEnumName(propNode, parentName, propName, enumSuffix) {
|
|
918
|
-
const enumNode = narrowSchema(propNode, "enum");
|
|
919
|
-
if (enumNode?.primitive === "boolean") return {
|
|
920
|
-
...propNode,
|
|
921
|
-
name: void 0
|
|
922
|
-
};
|
|
923
|
-
if (enumNode) return {
|
|
924
|
-
...propNode,
|
|
925
|
-
name: resolveEnumPropName(parentName, propName, enumSuffix)
|
|
926
|
-
};
|
|
927
|
-
return propNode;
|
|
928
|
-
}
|
|
929
|
-
//#endregion
|
|
930
|
-
//#region src/refResolver.ts
|
|
931
924
|
/**
|
|
932
|
-
*
|
|
925
|
+
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
926
|
+
* Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
|
|
933
927
|
*/
|
|
934
|
-
function
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
928
|
+
function getDateType(options, format) {
|
|
929
|
+
if (!options.dateType) return null;
|
|
930
|
+
if (format === "date-time") {
|
|
931
|
+
if (options.dateType === "date") return {
|
|
932
|
+
type: "date",
|
|
933
|
+
representation: "date"
|
|
934
|
+
};
|
|
935
|
+
if (options.dateType === "stringOffset") return {
|
|
936
|
+
type: "datetime",
|
|
937
|
+
offset: true
|
|
938
|
+
};
|
|
939
|
+
if (options.dateType === "stringLocal") return {
|
|
940
|
+
type: "datetime",
|
|
941
|
+
local: true
|
|
942
|
+
};
|
|
940
943
|
return {
|
|
941
|
-
|
|
942
|
-
|
|
944
|
+
type: "datetime",
|
|
945
|
+
offset: false
|
|
943
946
|
};
|
|
944
|
-
}
|
|
947
|
+
}
|
|
948
|
+
if (format === "date") return {
|
|
949
|
+
type: "date",
|
|
950
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
951
|
+
};
|
|
952
|
+
return {
|
|
953
|
+
type: "time",
|
|
954
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
955
|
+
};
|
|
945
956
|
}
|
|
946
957
|
/**
|
|
947
|
-
*
|
|
958
|
+
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
948
959
|
*/
|
|
949
|
-
function
|
|
950
|
-
return
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const resolved = (resolveEnumName ?? resolveName)(schemaNode.name);
|
|
962
|
-
if (resolved) return {
|
|
963
|
-
...schemaNode,
|
|
964
|
-
name: resolved
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
} });
|
|
960
|
+
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
961
|
+
return {
|
|
962
|
+
name,
|
|
963
|
+
nullable,
|
|
964
|
+
title: schema.title,
|
|
965
|
+
description: schema.description,
|
|
966
|
+
deprecated: schema.deprecated,
|
|
967
|
+
readOnly: schema.readOnly,
|
|
968
|
+
writeOnly: schema.writeOnly,
|
|
969
|
+
default: defaultValue,
|
|
970
|
+
example: schema.example
|
|
971
|
+
};
|
|
968
972
|
}
|
|
969
973
|
//#endregion
|
|
970
974
|
//#region src/parser.ts
|
|
971
975
|
/**
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
976
|
+
* Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
|
|
977
|
+
* enum values into the items sub-schema. This pattern is technically invalid OAS
|
|
978
|
+
* but appears in the wild and must be handled gracefully.
|
|
975
979
|
*/
|
|
976
|
-
function
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (type === "boolean") return "boolean";
|
|
987
|
-
return "string";
|
|
988
|
-
}
|
|
989
|
-
/**
|
|
990
|
-
* Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
|
|
991
|
-
* Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
|
|
992
|
-
*/
|
|
993
|
-
function toMediaType(contentType) {
|
|
994
|
-
return Object.values(mediaTypes).includes(contentType) ? contentType : void 0;
|
|
980
|
+
function normalizeArrayEnum(schema) {
|
|
981
|
+
const normalizedItems = {
|
|
982
|
+
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
983
|
+
enum: schema.enum
|
|
984
|
+
};
|
|
985
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
986
|
+
return {
|
|
987
|
+
...schemaWithoutEnum,
|
|
988
|
+
items: normalizedItems
|
|
989
|
+
};
|
|
995
990
|
}
|
|
996
991
|
/**
|
|
997
|
-
*
|
|
998
|
-
* the `@kubb/ast` tree.
|
|
992
|
+
* Builds the internal converter functions for a given `OasParserContext`.
|
|
999
993
|
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
* This is the **kubb-parser** stage of the compilation lifecycle:
|
|
1004
|
-
* OpenAPI / Swagger → Kubb AST
|
|
1005
|
-
*
|
|
1006
|
-
* No code is generated here; the resulting tree is spec-agnostic and can
|
|
1007
|
-
* be consumed by any downstream plugin (plugin-ts, plugin-zod, …).
|
|
1008
|
-
*
|
|
1009
|
-
* @example
|
|
1010
|
-
* ```ts
|
|
1011
|
-
* const parser = createOasParser(oas)
|
|
1012
|
-
* const root = parser.parse({ emptySchemaType: 'unknown' })
|
|
1013
|
-
* ```
|
|
994
|
+
* All `convert*` functions are defined as function declarations so they can freely
|
|
995
|
+
* reference each other and `parseSchema` via JS hoisting (mutual recursion).
|
|
1014
996
|
*/
|
|
1015
|
-
function
|
|
1016
|
-
const
|
|
1017
|
-
const TYPE_OPTION_MAP = {
|
|
1018
|
-
any: schemaTypes.any,
|
|
1019
|
-
unknown: schemaTypes.unknown,
|
|
1020
|
-
void: schemaTypes.void
|
|
1021
|
-
};
|
|
1022
|
-
/**
|
|
1023
|
-
* Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
|
|
1024
|
-
* Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
|
|
1025
|
-
*/
|
|
1026
|
-
function resolveTypeOption(value) {
|
|
1027
|
-
return TYPE_OPTION_MAP[value];
|
|
1028
|
-
}
|
|
1029
|
-
function normalizeArrayEnum(schema) {
|
|
1030
|
-
const normalizedItems = {
|
|
1031
|
-
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1032
|
-
enum: schema.enum
|
|
1033
|
-
};
|
|
1034
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1035
|
-
return {
|
|
1036
|
-
...schemaWithoutEnum,
|
|
1037
|
-
items: normalizedItems
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
/**
|
|
1041
|
-
* Resolves the AST type and datetime modifiers for a date/time format, honoring the `dateType` option.
|
|
1042
|
-
* Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
|
|
1043
|
-
*/
|
|
1044
|
-
function getDateType(options, format) {
|
|
1045
|
-
if (!options.dateType) return;
|
|
1046
|
-
if (format === "date-time") {
|
|
1047
|
-
if (options.dateType === "date") return {
|
|
1048
|
-
type: "date",
|
|
1049
|
-
representation: "date"
|
|
1050
|
-
};
|
|
1051
|
-
if (options.dateType === "stringOffset") return {
|
|
1052
|
-
type: "datetime",
|
|
1053
|
-
offset: true
|
|
1054
|
-
};
|
|
1055
|
-
if (options.dateType === "stringLocal") return {
|
|
1056
|
-
type: "datetime",
|
|
1057
|
-
local: true
|
|
1058
|
-
};
|
|
1059
|
-
return {
|
|
1060
|
-
type: "datetime",
|
|
1061
|
-
offset: false
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
if (format === "date") return {
|
|
1065
|
-
type: "date",
|
|
1066
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1067
|
-
};
|
|
1068
|
-
return {
|
|
1069
|
-
type: "time",
|
|
1070
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
997
|
+
function createSchemaParser(ctx) {
|
|
998
|
+
const document = ctx.document;
|
|
1073
999
|
/**
|
|
1074
|
-
*
|
|
1075
|
-
* Centralizes the common properties so sub-handlers don't repeat them.
|
|
1076
|
-
*/
|
|
1077
|
-
function renderSchemaBase(schema, name, nullable, defaultValue) {
|
|
1078
|
-
return {
|
|
1079
|
-
name,
|
|
1080
|
-
nullable,
|
|
1081
|
-
title: schema.title,
|
|
1082
|
-
description: schema.description,
|
|
1083
|
-
deprecated: schema.deprecated,
|
|
1084
|
-
readOnly: schema.readOnly,
|
|
1085
|
-
writeOnly: schema.writeOnly,
|
|
1086
|
-
default: defaultValue,
|
|
1087
|
-
example: schema.example
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Converts a `$ref` schema pointer into a `RefSchemaNode`.
|
|
1092
|
-
*
|
|
1093
|
-
* In OAS 3.0 siblings of `$ref` are technically ignored by the spec, but Kubb intentionally
|
|
1094
|
-
* preserves them so that annotations like `pattern`, `description`, and `nullable` are
|
|
1095
|
-
* reflected in generated JSDoc and type modifiers.
|
|
1000
|
+
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1096
1001
|
*/
|
|
1097
1002
|
function convertRef({ schema, nullable, defaultValue }) {
|
|
1098
1003
|
return createSchema({
|
|
@@ -1110,24 +1015,15 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1110
1015
|
});
|
|
1111
1016
|
}
|
|
1112
1017
|
/**
|
|
1113
|
-
* Converts
|
|
1114
|
-
* or an `IntersectionSchemaNode` (multi-member `allOf`).
|
|
1115
|
-
*
|
|
1116
|
-
* Single-member `allOf` without sibling structural keys is the common OAS 3.0 pattern for
|
|
1117
|
-
* annotating a `$ref` or primitive with extra constraints; it is flattened to avoid
|
|
1118
|
-
* producing needless intersection wrappers.
|
|
1119
|
-
*
|
|
1120
|
-
* The flatten path is skipped when the outer schema carries structural keys that cannot be
|
|
1121
|
-
* merged into annotation fields: `properties`, `required`, or `additionalProperties`.
|
|
1122
|
-
* Those cases must become an intersection so the constraints are preserved.
|
|
1123
|
-
*
|
|
1124
|
-
* Circular references through discriminator parents are detected and skipped to prevent
|
|
1125
|
-
* infinite recursion during code generation.
|
|
1018
|
+
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1126
1019
|
*/
|
|
1127
1020
|
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1128
1021
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1129
1022
|
const [memberSchema] = schema.allOf;
|
|
1130
|
-
const memberNode =
|
|
1023
|
+
const memberNode = parseSchema({
|
|
1024
|
+
schema: memberSchema,
|
|
1025
|
+
name: null
|
|
1026
|
+
}, rawOptions);
|
|
1131
1027
|
const { kind: _kind, ...memberNodeProps } = memberNode;
|
|
1132
1028
|
const mergedNullable = nullable || memberNode.nullable || void 0;
|
|
1133
1029
|
const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
|
|
@@ -1148,7 +1044,7 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1148
1044
|
const filteredDiscriminantValues = [];
|
|
1149
1045
|
const allOfMembers = schema.allOf.filter((item) => {
|
|
1150
1046
|
if (!isReference(item) || !name) return true;
|
|
1151
|
-
const deref =
|
|
1047
|
+
const deref = resolveRef(document, item.$ref);
|
|
1152
1048
|
if (!deref || !isDiscriminator(deref)) return true;
|
|
1153
1049
|
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1154
1050
|
if (!parentUnion) return true;
|
|
@@ -1156,7 +1052,7 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1156
1052
|
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1157
1053
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1158
1054
|
if (inOneOf || inMapping) {
|
|
1159
|
-
const discriminatorValue =
|
|
1055
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1160
1056
|
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1161
1057
|
propertyName: deref.discriminator.propertyName,
|
|
1162
1058
|
value: discriminatorValue
|
|
@@ -1164,7 +1060,7 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1164
1060
|
return false;
|
|
1165
1061
|
}
|
|
1166
1062
|
return true;
|
|
1167
|
-
}).map((s) =>
|
|
1063
|
+
}).map((s) => parseSchema({ schema: s }, rawOptions));
|
|
1168
1064
|
const syntheticStart = allOfMembers.length;
|
|
1169
1065
|
if (Array.isArray(schema.required) && schema.required.length) {
|
|
1170
1066
|
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
|
|
@@ -1172,11 +1068,11 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1172
1068
|
if (missingRequired.length) {
|
|
1173
1069
|
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1174
1070
|
if (!isReference(item)) return [item];
|
|
1175
|
-
const deref =
|
|
1071
|
+
const deref = resolveRef(document, item.$ref);
|
|
1176
1072
|
return deref && !isReference(deref) ? [deref] : [];
|
|
1177
1073
|
});
|
|
1178
1074
|
for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
|
|
1179
|
-
allOfMembers.push(
|
|
1075
|
+
allOfMembers.push(parseSchema({ schema: {
|
|
1180
1076
|
properties: { [key]: resolved.properties[key] },
|
|
1181
1077
|
required: [key]
|
|
1182
1078
|
} }, rawOptions));
|
|
@@ -1186,69 +1082,82 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1186
1082
|
}
|
|
1187
1083
|
if (schema.properties) {
|
|
1188
1084
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1189
|
-
allOfMembers.push(
|
|
1085
|
+
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1190
1086
|
}
|
|
1191
|
-
for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode(
|
|
1087
|
+
for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1088
|
+
propertyName,
|
|
1089
|
+
value
|
|
1090
|
+
}));
|
|
1192
1091
|
return createSchema({
|
|
1193
1092
|
type: "intersection",
|
|
1194
|
-
members: [...
|
|
1195
|
-
...
|
|
1093
|
+
members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
|
|
1094
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1196
1095
|
});
|
|
1197
1096
|
}
|
|
1198
1097
|
/**
|
|
1199
1098
|
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
1200
|
-
*
|
|
1201
|
-
* Both keywords are treated identically — their members are concatenated into a single union.
|
|
1202
|
-
* When sibling `properties` are present alongside `oneOf`/`anyOf`, each union member is
|
|
1203
|
-
* individually intersected with the shared properties node to match the OAS pattern of
|
|
1204
|
-
* adding common fields next to a discriminated union.
|
|
1205
1099
|
*/
|
|
1206
1100
|
function convertUnion({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1101
|
+
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1102
|
+
const discriminatorProperty = narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1103
|
+
if (!discriminatorProperty) return null;
|
|
1104
|
+
return createSchema({
|
|
1105
|
+
type: "object",
|
|
1106
|
+
primitive: "object",
|
|
1107
|
+
properties: [discriminatorProperty]
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1207
1110
|
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1208
1111
|
const unionBase = {
|
|
1209
|
-
...
|
|
1112
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1210
1113
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0
|
|
1211
1114
|
};
|
|
1212
1115
|
const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
1213
1116
|
const sharedPropertiesNode = schema.properties ? (() => {
|
|
1214
1117
|
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
|
|
1215
|
-
return
|
|
1118
|
+
return parseSchema({
|
|
1216
1119
|
schema: discriminator ? Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== "discriminator")) : schemaWithoutUnion,
|
|
1217
1120
|
name
|
|
1218
1121
|
}, rawOptions);
|
|
1219
1122
|
})() : void 0;
|
|
1220
|
-
if (sharedPropertiesNode || discriminator?.mapping)
|
|
1221
|
-
|
|
1222
|
-
...unionBase,
|
|
1223
|
-
members: unionMembers.map((s) => {
|
|
1123
|
+
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
1124
|
+
const members = unionMembers.map((s) => {
|
|
1224
1125
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1225
|
-
const discriminatorValue =
|
|
1226
|
-
const memberNode =
|
|
1227
|
-
if (
|
|
1126
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref);
|
|
1127
|
+
const memberNode = parseSchema({ schema: s }, rawOptions);
|
|
1128
|
+
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1129
|
+
return createSchema({
|
|
1228
1130
|
type: "intersection",
|
|
1229
|
-
members: [memberNode,
|
|
1131
|
+
members: [memberNode, (sharedPropertiesNode ? pickDiscriminatorPropertyNode(setDiscriminatorEnum({
|
|
1230
1132
|
node: sharedPropertiesNode,
|
|
1231
1133
|
propertyName: discriminator.propertyName,
|
|
1232
1134
|
values: [discriminatorValue]
|
|
1233
|
-
}) :
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
type: "intersection",
|
|
1238
|
-
members: [memberNode, createDiscriminantNode(discriminator.propertyName, discriminatorValue)]
|
|
1135
|
+
}), discriminator.propertyName) : void 0) ?? createDiscriminantNode({
|
|
1136
|
+
propertyName: discriminator.propertyName,
|
|
1137
|
+
value: discriminatorValue
|
|
1138
|
+
})]
|
|
1239
1139
|
});
|
|
1240
|
-
})
|
|
1241
|
-
|
|
1140
|
+
});
|
|
1141
|
+
const unionNode = createSchema({
|
|
1142
|
+
type: "union",
|
|
1143
|
+
...unionBase,
|
|
1144
|
+
members
|
|
1145
|
+
});
|
|
1146
|
+
if (!sharedPropertiesNode) return unionNode;
|
|
1147
|
+
return createSchema({
|
|
1148
|
+
type: "intersection",
|
|
1149
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1150
|
+
members: [unionNode, sharedPropertiesNode]
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1242
1153
|
return createSchema({
|
|
1243
1154
|
type: "union",
|
|
1244
1155
|
...unionBase,
|
|
1245
|
-
members:
|
|
1156
|
+
members: simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s }, rawOptions)))
|
|
1246
1157
|
});
|
|
1247
1158
|
}
|
|
1248
1159
|
/**
|
|
1249
|
-
* Converts an OAS 3.1 `const` schema into
|
|
1250
|
-
* `const: null` maps to a null scalar; any other value becomes a one-item enum so that generators
|
|
1251
|
-
* can produce a precise literal type.
|
|
1160
|
+
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1252
1161
|
*/
|
|
1253
1162
|
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1254
1163
|
const constValue = schema.const;
|
|
@@ -1264,16 +1173,15 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1264
1173
|
type: "enum",
|
|
1265
1174
|
primitive: getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string"),
|
|
1266
1175
|
enumValues: [constValue],
|
|
1267
|
-
...
|
|
1176
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1268
1177
|
});
|
|
1269
1178
|
}
|
|
1270
1179
|
/**
|
|
1271
|
-
*
|
|
1272
|
-
* Returns `
|
|
1273
|
-
* (i.e. `format: 'date-time'` with `dateType: false`).
|
|
1180
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
1181
|
+
* Returns `null` when the format should fall through to string handling (`dateType: false`).
|
|
1274
1182
|
*/
|
|
1275
1183
|
function convertFormat({ schema, name, nullable, defaultValue, options }) {
|
|
1276
|
-
const base =
|
|
1184
|
+
const base = buildSchemaNode(schema, name, nullable, defaultValue);
|
|
1277
1185
|
if (schema.format === "int64") return createSchema({
|
|
1278
1186
|
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1279
1187
|
primitive: "integer",
|
|
@@ -1285,7 +1193,7 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1285
1193
|
});
|
|
1286
1194
|
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1287
1195
|
const dateType = getDateType(options, schema.format);
|
|
1288
|
-
if (!dateType) return
|
|
1196
|
+
if (!dateType) return null;
|
|
1289
1197
|
if (dateType.type === "datetime") return createSchema({
|
|
1290
1198
|
...base,
|
|
1291
1199
|
primitive: "string",
|
|
@@ -1300,8 +1208,8 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1300
1208
|
representation: dateType.representation
|
|
1301
1209
|
});
|
|
1302
1210
|
}
|
|
1303
|
-
const specialType =
|
|
1304
|
-
if (!specialType) return
|
|
1211
|
+
const specialType = getSchemaType(schema.format);
|
|
1212
|
+
if (!specialType) return null;
|
|
1305
1213
|
const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
|
|
1306
1214
|
if (specialType === "number" || specialType === "integer" || specialType === "bigint") return createSchema({
|
|
1307
1215
|
...base,
|
|
@@ -1321,16 +1229,9 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1321
1229
|
}
|
|
1322
1230
|
/**
|
|
1323
1231
|
* Converts an `enum` schema into an `EnumSchemaNode`.
|
|
1324
|
-
*
|
|
1325
|
-
* Handles several edge cases:
|
|
1326
|
-
* - `{ type: 'array', enum }` (technically invalid OAS) — the enum is normalized into `items`.
|
|
1327
|
-
* - `null` in enum values (OAS 3.0 nullable enum convention) — stripped and reflected as `nullable`.
|
|
1328
|
-
* - `x-enumNames` / `x-enum-varnames` vendor extensions — produce named enum variants with explicit labels.
|
|
1329
|
-
* - Numeric and boolean enums require a const-map representation because most generators cannot
|
|
1330
|
-
* use string-enum syntax for non-string values.
|
|
1331
1232
|
*/
|
|
1332
1233
|
function convertEnum({ schema, name, nullable, type, rawOptions }) {
|
|
1333
|
-
if (type === "array") return
|
|
1234
|
+
if (type === "array") return parseSchema({
|
|
1334
1235
|
schema: normalizeArrayEnum(schema),
|
|
1335
1236
|
name
|
|
1336
1237
|
}, rawOptions);
|
|
@@ -1354,15 +1255,15 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1354
1255
|
};
|
|
1355
1256
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1356
1257
|
if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1357
|
-
const
|
|
1258
|
+
const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
|
|
1358
1259
|
const sourceValues = extensionKey ? [...new Set(schema[extensionKey])] : [...new Set(filteredValues)];
|
|
1359
1260
|
return createSchema({
|
|
1360
1261
|
...enumBase,
|
|
1361
|
-
|
|
1262
|
+
primitive: enumPrimitiveType,
|
|
1362
1263
|
namedEnumValues: sourceValues.map((label, index) => ({
|
|
1363
1264
|
name: String(label),
|
|
1364
1265
|
value: extensionKey ? filteredValues[index] ?? label : label,
|
|
1365
|
-
|
|
1266
|
+
primitive: enumPrimitiveType
|
|
1366
1267
|
}))
|
|
1367
1268
|
});
|
|
1368
1269
|
}
|
|
@@ -1372,29 +1273,20 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1372
1273
|
});
|
|
1373
1274
|
}
|
|
1374
1275
|
/**
|
|
1375
|
-
* Converts an object-like schema
|
|
1376
|
-
* or `patternProperties`) into an `ObjectSchemaNode`.
|
|
1377
|
-
*
|
|
1378
|
-
* When a `discriminator` is present, the discriminator property's schema is replaced with an
|
|
1379
|
-
* enum of the mapping keys so generators can produce a precise literal-union type for it.
|
|
1380
|
-
*
|
|
1381
|
-
* Property optionality follows OAS semantics:
|
|
1382
|
-
* - required + not nullable → `required: true`
|
|
1383
|
-
* - not required + not nullable → `optional: true`
|
|
1384
|
-
* - not required + nullable → `nullish: true`
|
|
1276
|
+
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
1385
1277
|
*/
|
|
1386
1278
|
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }) {
|
|
1387
1279
|
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
1388
1280
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
1389
1281
|
const resolvedPropSchema = propSchema;
|
|
1390
1282
|
const propNullable = isNullable(resolvedPropSchema);
|
|
1391
|
-
let schemaNode =
|
|
1283
|
+
let schemaNode = setEnumName(parseSchema({
|
|
1392
1284
|
schema: resolvedPropSchema,
|
|
1393
|
-
name:
|
|
1285
|
+
name: childName(name, propName)
|
|
1394
1286
|
}, rawOptions), name, propName, options.enumSuffix);
|
|
1395
1287
|
const tupleNode = narrowSchema(schemaNode, "tuple");
|
|
1396
1288
|
if (tupleNode?.items) {
|
|
1397
|
-
const namedItems = tupleNode.items.map((item) =>
|
|
1289
|
+
const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix));
|
|
1398
1290
|
if (namedItems.some((item, i) => item !== tupleNode.items[i])) schemaNode = {
|
|
1399
1291
|
...tupleNode,
|
|
1400
1292
|
items: namedItems
|
|
@@ -1412,73 +1304,65 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1412
1304
|
const additionalProperties = schema.additionalProperties;
|
|
1413
1305
|
let additionalPropertiesNode;
|
|
1414
1306
|
if (additionalProperties === true) additionalPropertiesNode = true;
|
|
1415
|
-
else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode =
|
|
1307
|
+
else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
|
|
1416
1308
|
else if (additionalProperties === false) additionalPropertiesNode = void 0;
|
|
1417
|
-
else if (additionalProperties) additionalPropertiesNode = createSchema({ type:
|
|
1309
|
+
else if (additionalProperties) additionalPropertiesNode = createSchema({ type: typeOptionMap.get(options.unknownType) });
|
|
1418
1310
|
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1419
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? createSchema({ type:
|
|
1311
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? createSchema({ type: typeOptionMap.get(options.unknownType) }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
|
|
1420
1312
|
const objectNode = createSchema({
|
|
1421
1313
|
type: "object",
|
|
1422
1314
|
primitive: "object",
|
|
1423
1315
|
properties,
|
|
1424
1316
|
additionalProperties: additionalPropertiesNode,
|
|
1425
1317
|
patternProperties,
|
|
1426
|
-
...
|
|
1318
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1427
1319
|
});
|
|
1428
1320
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1429
1321
|
const discPropName = schema.discriminator.propertyName;
|
|
1430
|
-
return
|
|
1322
|
+
return setDiscriminatorEnum({
|
|
1431
1323
|
node: objectNode,
|
|
1432
1324
|
propertyName: discPropName,
|
|
1433
1325
|
values: Object.keys(schema.discriminator.mapping),
|
|
1434
|
-
enumName: name ?
|
|
1326
|
+
enumName: name ? enumPropName(name, discPropName, options.enumSuffix) : void 0
|
|
1435
1327
|
});
|
|
1436
1328
|
}
|
|
1437
1329
|
return objectNode;
|
|
1438
1330
|
}
|
|
1439
1331
|
/**
|
|
1440
1332
|
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1441
|
-
*
|
|
1442
|
-
* Each `prefixItems` element maps to a positional tuple slot. When an explicit `items` schema
|
|
1443
|
-
* is present alongside `prefixItems`, it becomes the rest element. When `items` is absent,
|
|
1444
|
-
* a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
|
|
1445
|
-
* means additional items are allowed).
|
|
1446
1333
|
*/
|
|
1447
1334
|
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1448
1335
|
return createSchema({
|
|
1449
1336
|
type: "tuple",
|
|
1450
1337
|
primitive: "array",
|
|
1451
|
-
items: (schema.prefixItems ?? []).map((item) =>
|
|
1452
|
-
rest: schema.items ?
|
|
1338
|
+
items: (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions)),
|
|
1339
|
+
rest: schema.items ? parseSchema({ schema: schema.items }, rawOptions) : createSchema({ type: "any" }),
|
|
1453
1340
|
min: schema.minItems,
|
|
1454
1341
|
max: schema.maxItems,
|
|
1455
|
-
...
|
|
1342
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1456
1343
|
});
|
|
1457
1344
|
}
|
|
1458
1345
|
/**
|
|
1459
1346
|
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1460
|
-
*
|
|
1461
|
-
* When the items schema is an inline enum, a name derived from the parent array's name and
|
|
1462
|
-
* `enumSuffix` is forwarded so generators can emit a named enum declaration.
|
|
1463
1347
|
*/
|
|
1464
1348
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }) {
|
|
1465
1349
|
const rawItems = schema.items;
|
|
1466
|
-
const itemName = rawItems?.enum?.length && name ?
|
|
1350
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(void 0, name, options.enumSuffix) : void 0;
|
|
1467
1351
|
return createSchema({
|
|
1468
1352
|
type: "array",
|
|
1469
1353
|
primitive: "array",
|
|
1470
|
-
items: rawItems ? [
|
|
1354
|
+
items: rawItems ? [parseSchema({
|
|
1471
1355
|
schema: rawItems,
|
|
1472
1356
|
name: itemName
|
|
1473
1357
|
}, rawOptions)] : [],
|
|
1474
1358
|
min: schema.minItems,
|
|
1475
1359
|
max: schema.maxItems,
|
|
1476
1360
|
unique: schema.uniqueItems ?? void 0,
|
|
1477
|
-
...
|
|
1361
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1478
1362
|
});
|
|
1479
1363
|
}
|
|
1480
1364
|
/**
|
|
1481
|
-
* Converts a `type: 'string'` schema
|
|
1365
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1482
1366
|
*/
|
|
1483
1367
|
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1484
1368
|
return createSchema({
|
|
@@ -1487,11 +1371,11 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1487
1371
|
min: schema.minLength,
|
|
1488
1372
|
max: schema.maxLength,
|
|
1489
1373
|
pattern: schema.pattern,
|
|
1490
|
-
...
|
|
1374
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1491
1375
|
});
|
|
1492
1376
|
}
|
|
1493
1377
|
/**
|
|
1494
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema
|
|
1378
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1495
1379
|
*/
|
|
1496
1380
|
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1497
1381
|
return createSchema({
|
|
@@ -1501,21 +1385,21 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1501
1385
|
max: schema.maximum,
|
|
1502
1386
|
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1503
1387
|
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1504
|
-
...
|
|
1388
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1505
1389
|
});
|
|
1506
1390
|
}
|
|
1507
1391
|
/**
|
|
1508
|
-
* Converts a `type: 'boolean'` schema
|
|
1392
|
+
* Converts a `type: 'boolean'` schema.
|
|
1509
1393
|
*/
|
|
1510
1394
|
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1511
1395
|
return createSchema({
|
|
1512
1396
|
type: "boolean",
|
|
1513
1397
|
primitive: "boolean",
|
|
1514
|
-
...
|
|
1398
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1515
1399
|
});
|
|
1516
1400
|
}
|
|
1517
1401
|
/**
|
|
1518
|
-
* Converts an explicit `type: 'null'`
|
|
1402
|
+
* Converts an explicit `type: 'null'` schema.
|
|
1519
1403
|
*/
|
|
1520
1404
|
function convertNull({ schema, name, nullable }) {
|
|
1521
1405
|
return createSchema({
|
|
@@ -1529,28 +1413,19 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1529
1413
|
});
|
|
1530
1414
|
}
|
|
1531
1415
|
/**
|
|
1532
|
-
* Central dispatcher
|
|
1416
|
+
* Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1533
1417
|
*
|
|
1534
|
-
* Dispatch order (first match wins):
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* 3. `oneOf` / `anyOf` union
|
|
1538
|
-
* 4. `const` literal (OAS 3.1)
|
|
1539
|
-
* 5. `format`-based special type (date/time, uuid, blob, …)
|
|
1540
|
-
* 6. OAS 3.1 `contentMediaType: 'application/octet-stream'` blob
|
|
1541
|
-
* 7. OAS 3.1 multi-type array → union or fallthrough
|
|
1542
|
-
* 8. Constraint-inferred type (minLength/maxLength → string; minimum/maximum → number)
|
|
1543
|
-
* 9. `enum` values
|
|
1544
|
-
* 10. Object / array / tuple / scalar by `type`
|
|
1545
|
-
* 11. Empty schema fallback (`emptySchemaType` option)
|
|
1418
|
+
* Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
|
|
1419
|
+
* → octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
|
|
1420
|
+
* → empty-schema fallback (`emptySchemaType` option).
|
|
1546
1421
|
*/
|
|
1547
|
-
function
|
|
1422
|
+
function parseSchema({ schema, name }, rawOptions) {
|
|
1548
1423
|
const options = {
|
|
1549
1424
|
...DEFAULT_PARSER_OPTIONS,
|
|
1550
1425
|
...rawOptions
|
|
1551
1426
|
};
|
|
1552
1427
|
const flattenedSchema = flattenSchema(schema);
|
|
1553
|
-
if (flattenedSchema && flattenedSchema !== schema) return
|
|
1428
|
+
if (flattenedSchema && flattenedSchema !== schema) return parseSchema({
|
|
1554
1429
|
schema: flattenedSchema,
|
|
1555
1430
|
name
|
|
1556
1431
|
}, rawOptions);
|
|
@@ -1577,21 +1452,21 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1577
1452
|
if (schema.type === "string" && schema.contentMediaType === "application/octet-stream") return createSchema({
|
|
1578
1453
|
type: "blob",
|
|
1579
1454
|
primitive: "string",
|
|
1580
|
-
...
|
|
1455
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1581
1456
|
});
|
|
1582
1457
|
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
1583
1458
|
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
1584
1459
|
const arrayNullable = schema.type.includes("null") || nullable || void 0;
|
|
1585
1460
|
if (nonNullTypes.length > 1) return createSchema({
|
|
1586
1461
|
type: "union",
|
|
1587
|
-
members: nonNullTypes.map((t) =>
|
|
1462
|
+
members: nonNullTypes.map((t) => parseSchema({
|
|
1588
1463
|
schema: {
|
|
1589
1464
|
...schema,
|
|
1590
1465
|
type: t
|
|
1591
1466
|
},
|
|
1592
1467
|
name
|
|
1593
1468
|
}, rawOptions)),
|
|
1594
|
-
...
|
|
1469
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue)
|
|
1595
1470
|
});
|
|
1596
1471
|
}
|
|
1597
1472
|
if (!type) {
|
|
@@ -1608,19 +1483,18 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1608
1483
|
if (type === "boolean") return convertBoolean(ctx);
|
|
1609
1484
|
if (type === "null") return convertNull(ctx);
|
|
1610
1485
|
return createSchema({
|
|
1611
|
-
type:
|
|
1486
|
+
type: typeOptionMap.get(options.emptySchemaType),
|
|
1612
1487
|
name,
|
|
1613
1488
|
title: schema.title,
|
|
1614
1489
|
description: schema.description
|
|
1615
1490
|
});
|
|
1616
1491
|
}
|
|
1617
1492
|
/**
|
|
1618
|
-
* Converts a
|
|
1619
|
-
* When the parameter has no `schema`, falls back to `unknownType`; `$ref` schemas are resolved through `convertSchema` to produce a proper named type reference.
|
|
1493
|
+
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
1620
1494
|
*/
|
|
1621
1495
|
function parseParameter(options, param) {
|
|
1622
1496
|
const required = param["required"] ?? false;
|
|
1623
|
-
const schema = param["schema"] ?
|
|
1497
|
+
const schema = param["schema"] ? parseSchema({ schema: param["schema"] }, options) : createSchema({ type: typeOptionMap.get(options.unknownType) });
|
|
1624
1498
|
return createParameter({
|
|
1625
1499
|
name: param["name"],
|
|
1626
1500
|
in: param["in"],
|
|
@@ -1632,13 +1506,12 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1632
1506
|
});
|
|
1633
1507
|
}
|
|
1634
1508
|
/**
|
|
1635
|
-
* Converts an OAS `Operation` into an `OperationNode
|
|
1636
|
-
* request body, and all response codes into their AST node equivalents.
|
|
1509
|
+
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1637
1510
|
*/
|
|
1638
|
-
function parseOperation(options,
|
|
1639
|
-
const parameters =
|
|
1640
|
-
const requestBodySchema =
|
|
1641
|
-
const requestBodySchemaNode = requestBodySchema ?
|
|
1511
|
+
function parseOperation(options, operation) {
|
|
1512
|
+
const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
|
|
1513
|
+
const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType });
|
|
1514
|
+
const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : void 0;
|
|
1642
1515
|
const requestBodyDescription = operation.schema.requestBody && !isReference(operation.schema.requestBody) ? operation.schema.requestBody.description : void 0;
|
|
1643
1516
|
const requestBodyKeysToOmit = requestBodySchema?.properties ? Object.entries(requestBodySchema.properties).filter(([, prop]) => !isReference(prop) && prop.readOnly).map(([key]) => key) : void 0;
|
|
1644
1517
|
const requestBody = requestBodySchemaNode ? {
|
|
@@ -1648,11 +1521,11 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1648
1521
|
} : void 0;
|
|
1649
1522
|
const responses = operation.getResponseStatusCodes().map((statusCode) => {
|
|
1650
1523
|
const responseObj = operation.getResponseByStatusCode(statusCode);
|
|
1651
|
-
const responseSchema =
|
|
1652
|
-
const schema = responseSchema && Object.keys(responseSchema).length > 0 ?
|
|
1524
|
+
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
|
|
1525
|
+
const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
|
|
1653
1526
|
const description = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : void 0;
|
|
1654
1527
|
const rawContent = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.content : void 0;
|
|
1655
|
-
const mediaType = rawContent ?
|
|
1528
|
+
const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? "") : getMediaType(operation.contentType ?? "");
|
|
1656
1529
|
const keysToOmit = responseSchema?.properties ? Object.entries(responseSchema.properties).filter(([, prop]) => !isReference(prop) && prop.writeOnly).map(([key]) => key) : void 0;
|
|
1657
1530
|
return createResponse({
|
|
1658
1531
|
statusCode,
|
|
@@ -1675,133 +1548,135 @@ function createOasParser(oas, { contentType } = {}) {
|
|
|
1675
1548
|
responses
|
|
1676
1549
|
});
|
|
1677
1550
|
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
|
|
1680
|
-
* a `RootNode` — the top-level node of the `@kubb/ast` tree.
|
|
1681
|
-
*/
|
|
1682
|
-
function parse(options) {
|
|
1683
|
-
const mergedOptions = {
|
|
1684
|
-
...DEFAULT_PARSER_OPTIONS,
|
|
1685
|
-
...options
|
|
1686
|
-
};
|
|
1687
|
-
const schemas = Object.entries(schemaObjects).map(([name, schemaObject]) => convertSchema({
|
|
1688
|
-
schema: schemaObject,
|
|
1689
|
-
name
|
|
1690
|
-
}, mergedOptions));
|
|
1691
|
-
const paths = oas.getPaths();
|
|
1692
|
-
return createRoot({
|
|
1693
|
-
schemas,
|
|
1694
|
-
operations: Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? parseOperation(mergedOptions, oas, operation) : null).filter((op) => op !== null))
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
1551
|
return {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1552
|
+
parseSchema,
|
|
1553
|
+
parseOperation,
|
|
1554
|
+
parseParameter
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Converts the entire OpenAPI spec into a `RootNode` (the top-level `@kubb/ast` tree).
|
|
1559
|
+
*
|
|
1560
|
+
* This is the main entry point: `OpenAPI / Swagger → Kubb AST`.
|
|
1561
|
+
* No code is generated here — the resulting tree is spec-agnostic and consumed by
|
|
1562
|
+
* downstream plugins (`plugin-ts`, `plugin-zod`, …).
|
|
1563
|
+
*
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```ts
|
|
1566
|
+
* const document = await parseFromConfig(config)
|
|
1567
|
+
* const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
|
|
1568
|
+
* ```
|
|
1569
|
+
*/
|
|
1570
|
+
function parseOas(document, options = {}) {
|
|
1571
|
+
const { contentType, ...parserOptions } = options;
|
|
1572
|
+
const mergedOptions = {
|
|
1573
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
1574
|
+
...parserOptions
|
|
1575
|
+
};
|
|
1576
|
+
const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType });
|
|
1577
|
+
const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({
|
|
1578
|
+
document,
|
|
1579
|
+
contentType
|
|
1580
|
+
});
|
|
1581
|
+
const schemas = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({
|
|
1582
|
+
schema,
|
|
1583
|
+
name
|
|
1584
|
+
}, mergedOptions));
|
|
1585
|
+
const paths = new BaseOas(document).getPaths();
|
|
1586
|
+
return {
|
|
1587
|
+
root: createRoot({
|
|
1588
|
+
schemas,
|
|
1589
|
+
operations: Object.entries(paths).flatMap(([_path, methods]) => Object.entries(methods).map(([, operation]) => operation ? _parseOperation(mergedOptions, operation) : null).filter((op) => op !== null))
|
|
1590
|
+
}),
|
|
1706
1591
|
nameMapping
|
|
1707
1592
|
};
|
|
1708
1593
|
}
|
|
1709
1594
|
//#endregion
|
|
1710
1595
|
//#region src/adapter.ts
|
|
1596
|
+
/**
|
|
1597
|
+
* Stable string identifier for the OAS adapter used in Kubb's adapter registry.
|
|
1598
|
+
*/
|
|
1711
1599
|
const adapterOasName = "oas";
|
|
1712
1600
|
/**
|
|
1713
|
-
* Creates
|
|
1601
|
+
* Creates the default OpenAPI / Swagger adapter for Kubb.
|
|
1714
1602
|
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1603
|
+
* Parses the spec, optionally validates it, resolves the base URL, and converts
|
|
1604
|
+
* everything into a `RootNode` that downstream plugins consume.
|
|
1717
1605
|
*
|
|
1718
1606
|
* @example
|
|
1719
1607
|
* ```ts
|
|
1720
1608
|
* import { defineConfig } from '@kubb/core'
|
|
1721
1609
|
* import { adapterOas } from '@kubb/adapter-oas'
|
|
1610
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1722
1611
|
*
|
|
1723
1612
|
* export default defineConfig({
|
|
1724
|
-
* adapter: adapterOas({
|
|
1725
|
-
* input:
|
|
1726
|
-
* plugins: [pluginTs()
|
|
1613
|
+
* adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
|
|
1614
|
+
* input: { path: './openapi.yaml' },
|
|
1615
|
+
* plugins: [pluginTs()],
|
|
1727
1616
|
* })
|
|
1728
1617
|
* ```
|
|
1729
1618
|
*/
|
|
1730
1619
|
const adapterOas = createAdapter((options) => {
|
|
1731
|
-
const { validate = true,
|
|
1732
|
-
|
|
1620
|
+
const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", 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;
|
|
1621
|
+
let nameMapping = /* @__PURE__ */ new Map();
|
|
1733
1622
|
return {
|
|
1734
1623
|
name: "oas",
|
|
1735
|
-
options
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1624
|
+
get options() {
|
|
1625
|
+
return {
|
|
1626
|
+
validate,
|
|
1627
|
+
contentType,
|
|
1628
|
+
serverIndex,
|
|
1629
|
+
serverVariables,
|
|
1630
|
+
discriminator,
|
|
1631
|
+
dateType,
|
|
1632
|
+
integerType,
|
|
1633
|
+
unknownType,
|
|
1634
|
+
emptySchemaType,
|
|
1635
|
+
enumSuffix,
|
|
1636
|
+
nameMapping
|
|
1637
|
+
};
|
|
1748
1638
|
},
|
|
1749
1639
|
getImports(node, resolve) {
|
|
1750
|
-
return
|
|
1640
|
+
return collectImports({
|
|
1751
1641
|
node,
|
|
1752
1642
|
nameMapping,
|
|
1753
|
-
resolve
|
|
1643
|
+
resolve: (schemaName) => {
|
|
1644
|
+
const result = resolve(schemaName);
|
|
1645
|
+
if (!result) return;
|
|
1646
|
+
return {
|
|
1647
|
+
name: [result.name],
|
|
1648
|
+
path: result.path
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1754
1651
|
});
|
|
1755
1652
|
},
|
|
1756
1653
|
async parse(source) {
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
discriminator
|
|
1761
|
-
});
|
|
1762
|
-
if (validate) try {
|
|
1763
|
-
await oas.validate();
|
|
1764
|
-
} catch (_err) {}
|
|
1765
|
-
const server = serverIndex !== void 0 ? oas.api.servers?.at(serverIndex) : void 0;
|
|
1654
|
+
const document = await parseFromConfig(source);
|
|
1655
|
+
if (validate) await validateDocument(document);
|
|
1656
|
+
const server = serverIndex !== void 0 ? document.servers?.at(serverIndex) : void 0;
|
|
1766
1657
|
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : void 0;
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1658
|
+
const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
|
|
1659
|
+
contentType,
|
|
1769
1660
|
dateType,
|
|
1770
1661
|
integerType,
|
|
1771
1662
|
unknownType,
|
|
1772
1663
|
emptySchemaType,
|
|
1773
1664
|
enumSuffix
|
|
1774
1665
|
});
|
|
1775
|
-
|
|
1776
|
-
|
|
1666
|
+
const root = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
|
|
1667
|
+
nameMapping = parsedNameMapping;
|
|
1777
1668
|
return createRoot({
|
|
1778
1669
|
...root,
|
|
1779
1670
|
meta: {
|
|
1780
|
-
title:
|
|
1781
|
-
description:
|
|
1782
|
-
version:
|
|
1671
|
+
title: document.info?.title,
|
|
1672
|
+
description: document.info?.description,
|
|
1673
|
+
version: document.info?.version,
|
|
1783
1674
|
baseURL
|
|
1784
1675
|
}
|
|
1785
1676
|
});
|
|
1786
1677
|
}
|
|
1787
1678
|
};
|
|
1788
1679
|
});
|
|
1789
|
-
function sourceToFakeConfig(source) {
|
|
1790
|
-
switch (source.type) {
|
|
1791
|
-
case "path": return {
|
|
1792
|
-
root: path.dirname(source.path),
|
|
1793
|
-
input: { path: source.path }
|
|
1794
|
-
};
|
|
1795
|
-
case "data": return {
|
|
1796
|
-
root: process.cwd(),
|
|
1797
|
-
input: { data: source.data }
|
|
1798
|
-
};
|
|
1799
|
-
case "paths": return {
|
|
1800
|
-
root: source.paths[0] ? path.dirname(source.paths[0]) : process.cwd(),
|
|
1801
|
-
input: source.paths.map((p) => ({ path: p }))
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
1680
|
//#endregion
|
|
1806
1681
|
export { adapterOas, adapterOasName };
|
|
1807
1682
|
|