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