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