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