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