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