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