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