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