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