@kubb/adapter-oas 5.15.3 → 5.16.0
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/README.md +1 -1
- package/dist/index.cjs +377 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -8
- package/dist/index.d.ts +20 -8
- package/dist/index.js +378 -47
- package/dist/index.js.map +1 -1
- package/docs/options.md +1 -1
- package/kubb-extension.json +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineExtension } from "@kubb/core";
|
|
2
|
-
import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName,
|
|
2
|
+
import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDedupeUnion, macroDiscriminatorEnum, macroEnumName, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { access, readFile } from "node:fs/promises";
|
|
5
5
|
import { upgrade } from "@scalar/openapi-upgrader";
|
|
@@ -19,7 +19,7 @@ var compatibility = {
|
|
|
19
19
|
"node": ">=22.22"
|
|
20
20
|
};
|
|
21
21
|
// package.json
|
|
22
|
-
var version = "5.
|
|
22
|
+
var version = "5.16.0";
|
|
23
23
|
// src/constants.ts
|
|
24
24
|
/**
|
|
25
25
|
* Default parser options applied when no explicit options are provided.
|
|
@@ -59,6 +59,17 @@ const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
|
59
59
|
"query"
|
|
60
60
|
]);
|
|
61
61
|
/**
|
|
62
|
+
* Media types OpenAPI 3.2 reads as a stream of items (its "sequential media types"), each described by `itemSchema`.
|
|
63
|
+
*/
|
|
64
|
+
const STREAM_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
65
|
+
"application/jsonl",
|
|
66
|
+
"application/x-ndjson",
|
|
67
|
+
"application/json-seq",
|
|
68
|
+
"application/geo+json-seq",
|
|
69
|
+
"text/event-stream",
|
|
70
|
+
"multipart/mixed"
|
|
71
|
+
]);
|
|
72
|
+
/**
|
|
62
73
|
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
63
74
|
*
|
|
64
75
|
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
@@ -383,6 +394,14 @@ function isNullable(schema) {
|
|
|
383
394
|
return false;
|
|
384
395
|
}
|
|
385
396
|
/**
|
|
397
|
+
* Returns `true` for an OpenAPI 3.2 or later document. Swagger 2.0 and OpenAPI 3.0 documents are
|
|
398
|
+
* upgraded to 3.1 before parsing, so `openapi` is always set here.
|
|
399
|
+
*/
|
|
400
|
+
function isOpenApi32(document) {
|
|
401
|
+
const [major = 0, minor = 0] = String(document.openapi).split(".").map(Number);
|
|
402
|
+
return major > 3 || major === 3 && minor >= 2;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
386
405
|
* Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
|
|
387
406
|
*/
|
|
388
407
|
function pickExtensions(source) {
|
|
@@ -449,7 +468,8 @@ const jsonMimeFragments = [
|
|
|
449
468
|
* ```
|
|
450
469
|
*/
|
|
451
470
|
function isJsonMimeType(mimeType) {
|
|
452
|
-
|
|
471
|
+
const lower = mimeType.toLowerCase();
|
|
472
|
+
return jsonMimeFragments.some((fragment) => lower.includes(fragment));
|
|
453
473
|
}
|
|
454
474
|
// src/refs.ts
|
|
455
475
|
/**
|
|
@@ -615,8 +635,10 @@ async function readSource(sourcePath) {
|
|
|
615
635
|
async function resolveSource(sourcePath) {
|
|
616
636
|
const data = await readSource(sourcePath);
|
|
617
637
|
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
638
|
+
const pathname = urlRegExp.test(sourcePath) ? new URL(sourcePath).pathname : sourcePath;
|
|
618
639
|
return parseYaml(data, {
|
|
619
640
|
origin: sourcePath,
|
|
641
|
+
json: !/\.ya?ml$/i.test(pathname),
|
|
620
642
|
help: "Fix the syntax at the reported line, or check that `input` points at the spec and not an HTML page."
|
|
621
643
|
});
|
|
622
644
|
}
|
|
@@ -624,11 +646,19 @@ async function resolveSource(sourcePath) {
|
|
|
624
646
|
* Parses a YAML (or JSON) spec. A syntax error throws `KUBB_INVALID_DOCUMENT` naming `origin`,
|
|
625
647
|
* with the parser message, which keeps the line and column.
|
|
626
648
|
*
|
|
627
|
-
*
|
|
628
|
-
* `yaml
|
|
629
|
-
*
|
|
649
|
+
* With `json` (the default), a document that starts with `{` or `[` goes through `JSON.parse`
|
|
650
|
+
* first, about ten times faster than `js-yaml` on a JSON spec. A duplicate key then keeps the last
|
|
651
|
+
* value, as every JSON parser does. YAML flow syntax also starts with `{`, so a parse failure falls
|
|
652
|
+
* through to YAML. Pass `json: false` for a source known to be YAML.
|
|
653
|
+
*
|
|
654
|
+
* `js-yaml` parses about ten times faster than `yaml`, and rejects duplicate keys instead of
|
|
655
|
+
* keeping the last one. Its core schema keeps timestamps as strings and `<<` as a plain key, as
|
|
656
|
+
* `yaml` does for YAML 1.2.
|
|
630
657
|
*/
|
|
631
|
-
function parseYaml(data, { origin, help }) {
|
|
658
|
+
function parseYaml(data, { origin, help, json = true }) {
|
|
659
|
+
if (json && /^\s*[[{]/.test(data)) try {
|
|
660
|
+
return JSON.parse(data);
|
|
661
|
+
} catch {}
|
|
632
662
|
try {
|
|
633
663
|
const document = load(data, { schema: CORE_SCHEMA });
|
|
634
664
|
if (document !== void 0) return document;
|
|
@@ -863,6 +893,17 @@ function reportInvalidSpec(message) {
|
|
|
863
893
|
* ```
|
|
864
894
|
*/
|
|
865
895
|
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
896
|
+
if (isOpenApi32(document)) {
|
|
897
|
+
const diagnostic = {
|
|
898
|
+
code: Diagnostics.code.invalidSpec,
|
|
899
|
+
severity: "warning",
|
|
900
|
+
message: "The document was not validated: `@readme/openapi-parser` does not support OpenAPI 3.2 yet.",
|
|
901
|
+
help: "Set `validate: false` on the adapter to skip this check until the validator supports OpenAPI 3.2."
|
|
902
|
+
};
|
|
903
|
+
if (throwOnError) throw new Diagnostics.Error(diagnostic);
|
|
904
|
+
Diagnostics.report(diagnostic);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
866
907
|
const { compileErrors, validate } = await import("@readme/openapi-parser");
|
|
867
908
|
let result;
|
|
868
909
|
try {
|
|
@@ -892,6 +933,7 @@ function reportValidationErrors({ errors, additionalErrors }) {
|
|
|
892
933
|
* validating in place where worker threads are missing, as in edge runtimes.
|
|
893
934
|
*/
|
|
894
935
|
async function validateDocumentInWorker(document) {
|
|
936
|
+
if (isOpenApi32(document)) return validateDocument(document);
|
|
895
937
|
let message;
|
|
896
938
|
try {
|
|
897
939
|
const { Worker } = await import("node:worker_threads");
|
|
@@ -1162,6 +1204,123 @@ function resolveServerUrl(server, overrides) {
|
|
|
1162
1204
|
}
|
|
1163
1205
|
return url;
|
|
1164
1206
|
}
|
|
1207
|
+
// src/model/mediaType.ts
|
|
1208
|
+
/**
|
|
1209
|
+
* The media type without its parameters, so `text/event-stream; charset=utf-8` matches the bare name.
|
|
1210
|
+
*/
|
|
1211
|
+
function baseMediaType(contentType) {
|
|
1212
|
+
return contentType.split(";")[0].trim().toLowerCase();
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Returns `true` for a media type read as a stream of items: one that OpenAPI 3.2 lists, or any that declares `itemSchema`.
|
|
1216
|
+
*/
|
|
1217
|
+
function isStream(contentType, mediaType) {
|
|
1218
|
+
return STREAM_MEDIA_TYPES.has(baseMediaType(contentType)) || mediaType?.itemSchema !== void 0;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Whether the media type is `text/event-stream`, with or without parameters.
|
|
1222
|
+
*/
|
|
1223
|
+
function isEventStream(contentType) {
|
|
1224
|
+
return baseMediaType(contentType) === "text/event-stream";
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Replaces a `data` string that holds JSON with its `contentSchema`, so the event reads as parsed.
|
|
1228
|
+
*/
|
|
1229
|
+
function withParsedData(schema, refs) {
|
|
1230
|
+
const data = schema.properties?.data;
|
|
1231
|
+
const resolved = data && isReference(data) ? refs.resolve(data.$ref, { report: false }) : data;
|
|
1232
|
+
if (!resolved?.contentSchema || !resolved.contentMediaType || !isJsonMimeType(resolved.contentMediaType)) return schema;
|
|
1233
|
+
return {
|
|
1234
|
+
...schema,
|
|
1235
|
+
properties: {
|
|
1236
|
+
...schema.properties,
|
|
1237
|
+
data: resolved.contentSchema
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Keywords next to `$ref` that only annotate it, so a reference that carries nothing else stays a reference.
|
|
1243
|
+
*/
|
|
1244
|
+
const REF_ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
1245
|
+
"$ref",
|
|
1246
|
+
"summary",
|
|
1247
|
+
"description",
|
|
1248
|
+
"$comment"
|
|
1249
|
+
]);
|
|
1250
|
+
/**
|
|
1251
|
+
* Applies `extra` on top of `base` the way JSON Schema does: `properties` and `required` add up, other keywords of `extra` win.
|
|
1252
|
+
*/
|
|
1253
|
+
function mergeSchemas(base, extra) {
|
|
1254
|
+
const required = [.../* @__PURE__ */ new Set([...Array.isArray(base.required) ? base.required : [], ...Array.isArray(extra.required) ? extra.required : []])];
|
|
1255
|
+
return {
|
|
1256
|
+
type: "object",
|
|
1257
|
+
...base,
|
|
1258
|
+
...extra,
|
|
1259
|
+
properties: {
|
|
1260
|
+
...base.properties,
|
|
1261
|
+
...extra.properties
|
|
1262
|
+
},
|
|
1263
|
+
...required.length ? { required } : {}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Normalizes the OpenAPI 3.2 `text/event-stream` item schema into one schema per event.
|
|
1268
|
+
* - Keywords next to a `$ref` apply to the referenced schema.
|
|
1269
|
+
* - Shared `properties` merge into each inline `oneOf`/`anyOf` member.
|
|
1270
|
+
* - A JSON `data` string becomes its `contentSchema`.
|
|
1271
|
+
*/
|
|
1272
|
+
function toEventSchema(schema, refs) {
|
|
1273
|
+
if (isReference(schema)) {
|
|
1274
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => !REF_ANNOTATIONS.has(key)));
|
|
1275
|
+
const resolved = Object.keys(siblings).length ? refs.resolve(schema.$ref, { report: false }) : null;
|
|
1276
|
+
return resolved ? toEventSchema(mergeSchemas(resolved, siblings), refs) : schema;
|
|
1277
|
+
}
|
|
1278
|
+
const key = schema.oneOf?.length ? "oneOf" : schema.anyOf?.length ? "anyOf" : void 0;
|
|
1279
|
+
if (!key) return withParsedData(schema, refs);
|
|
1280
|
+
const members = schema[key];
|
|
1281
|
+
const { properties, required, ...rest } = schema;
|
|
1282
|
+
const merge = !!properties && !members.some(isReference);
|
|
1283
|
+
const shared = {
|
|
1284
|
+
properties,
|
|
1285
|
+
required
|
|
1286
|
+
};
|
|
1287
|
+
return {
|
|
1288
|
+
...merge ? rest : schema,
|
|
1289
|
+
[key]: members.map((member) => isReference(member) ? member : withParsedData(merge ? mergeSchemas(shared, member) : member, refs))
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Whether the schema, following `$ref`s, or a composition member of it declares an `event` or `data` property.
|
|
1294
|
+
*/
|
|
1295
|
+
function declaresEventFields(schema, refs, seen) {
|
|
1296
|
+
if (isReference(schema)) {
|
|
1297
|
+
if (seen.has(schema.$ref)) return false;
|
|
1298
|
+
seen.add(schema.$ref);
|
|
1299
|
+
const resolved = refs.resolve(schema.$ref, { report: false });
|
|
1300
|
+
return !!resolved && declaresEventFields(resolved, refs, seen);
|
|
1301
|
+
}
|
|
1302
|
+
if (schema.properties && (Object.hasOwn(schema.properties, "event") || Object.hasOwn(schema.properties, "data"))) return true;
|
|
1303
|
+
return [
|
|
1304
|
+
...schema.oneOf ?? [],
|
|
1305
|
+
...schema.anyOf ?? [],
|
|
1306
|
+
...schema.allOf ?? []
|
|
1307
|
+
].some((member) => declaresEventFields(member, refs, seen));
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Returns `'event'` when the item declares `event` or `data`, directly or in a composition member, else `'data'`.
|
|
1311
|
+
*/
|
|
1312
|
+
function getItemShape(schema, refs) {
|
|
1313
|
+
return declaresEventFields(schema, refs, /* @__PURE__ */ new Set()) ? "event" : "data";
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Returns the raw item schema: `itemSchema`, or `schema` on `text/event-stream` as 3.0 and 3.1 documents write one event.
|
|
1317
|
+
*/
|
|
1318
|
+
function getItemSchema({ contentType, mediaType, refs, document }) {
|
|
1319
|
+
if (!mediaType || !isStream(contentType, mediaType)) return null;
|
|
1320
|
+
const raw = mediaType.itemSchema ?? (isEventStream(contentType) && !isOpenApi32(document) ? mediaType.schema : void 0);
|
|
1321
|
+
if (!raw) return null;
|
|
1322
|
+
return isEventStream(contentType) ? toEventSchema(raw, refs) : raw;
|
|
1323
|
+
}
|
|
1165
1324
|
// src/emit/schemaShape.ts
|
|
1166
1325
|
/**
|
|
1167
1326
|
* Whether `format` maps to a dedicated type; `false` is what `KUBB_UNSUPPORTED_FORMAT` flags.
|
|
@@ -1274,6 +1433,13 @@ function slugify(value) {
|
|
|
1274
1433
|
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
1275
1434
|
}
|
|
1276
1435
|
/**
|
|
1436
|
+
* Returns the operation's JSON pointer, under `additionalOperations` for an OpenAPI 3.2 method outside the fixed fields.
|
|
1437
|
+
*/
|
|
1438
|
+
function getOperationPointer({ path, method }) {
|
|
1439
|
+
const pathPointer = `#/paths/${escapePointerToken(path)}`;
|
|
1440
|
+
return SUPPORTED_METHODS.has(method.toLowerCase()) ? `${pathPointer}/${method.toLowerCase()}` : `${pathPointer}/additionalOperations/${escapePointerToken(method)}`;
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1277
1443
|
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
1278
1444
|
*/
|
|
1279
1445
|
function getOperationId({ path, method, schema }) {
|
|
@@ -1326,7 +1492,7 @@ function getParameters({ operation, refs }) {
|
|
|
1326
1492
|
}] : [];
|
|
1327
1493
|
});
|
|
1328
1494
|
const pathPointer = `#/paths/${escapePointerToken(operation.path)}`;
|
|
1329
|
-
const operationParams = resolveParams(operation.schema?.parameters || [],
|
|
1495
|
+
const operationParams = resolveParams(operation.schema?.parameters || [], getOperationPointer(operation));
|
|
1330
1496
|
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? [], pathPointer);
|
|
1331
1497
|
const paramMap = /* @__PURE__ */ new Map();
|
|
1332
1498
|
for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
|
|
@@ -1359,17 +1525,59 @@ function getEncoding(encoding) {
|
|
|
1359
1525
|
});
|
|
1360
1526
|
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1361
1527
|
}
|
|
1362
|
-
/**
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1528
|
+
/**
|
|
1529
|
+
* Returns the media type entry for `contentType`, resolving a `$ref` into `components.mediaTypes`. `null` when absent.
|
|
1530
|
+
*/
|
|
1531
|
+
function getMediaType({ content, contentType, refs }) {
|
|
1532
|
+
return contentType ? refs.deref(content?.[contentType]) : null;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Returns the schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON
|
|
1536
|
+
* entry. For a stream this is the item schema, so `schema: string` next to `itemSchema` types one item.
|
|
1537
|
+
*/
|
|
1538
|
+
function getBodySchema({ content, contentType, refs, document }) {
|
|
1539
|
+
const entry = getMediaType({
|
|
1540
|
+
content,
|
|
1541
|
+
contentType,
|
|
1542
|
+
refs
|
|
1543
|
+
});
|
|
1544
|
+
if (!contentType || !entry) return null;
|
|
1545
|
+
const schema = getItemSchema({
|
|
1546
|
+
contentType,
|
|
1547
|
+
mediaType: entry,
|
|
1548
|
+
refs,
|
|
1549
|
+
document
|
|
1550
|
+
}) ?? entry.schema;
|
|
1551
|
+
const binary = entry.itemSchema ? void 0 : getBinaryFallbackSchema(contentType, schema);
|
|
1367
1552
|
if (binary) return binary;
|
|
1368
|
-
return
|
|
1553
|
+
return schema ? refs.derefKeepingRef(schema) : null;
|
|
1554
|
+
}
|
|
1555
|
+
/** The `stream` field of a content node, set when the media type is a stream. `node` is the parsed item schema. */
|
|
1556
|
+
function getStreamField({ contentType, mediaType, refs, document, node }) {
|
|
1557
|
+
if (!isStream(contentType, mediaType)) return {};
|
|
1558
|
+
const raw = getItemSchema({
|
|
1559
|
+
contentType,
|
|
1560
|
+
mediaType,
|
|
1561
|
+
refs,
|
|
1562
|
+
document
|
|
1563
|
+
});
|
|
1564
|
+
return { stream: {
|
|
1565
|
+
...raw ? { schema: node } : {},
|
|
1566
|
+
...raw && isEventStream(contentType) ? { shape: getItemShape(raw, refs) } : {}
|
|
1567
|
+
} };
|
|
1568
|
+
}
|
|
1569
|
+
/** The media type keyword the body schema was read from, for the diagnostic pointer. */
|
|
1570
|
+
function schemaKeyword(mediaType) {
|
|
1571
|
+
return mediaType?.itemSchema ? "itemSchema" : "schema";
|
|
1572
|
+
}
|
|
1573
|
+
/** Pointer of a media type entry, following a `$ref` into `components.mediaTypes` so a diagnostic lands on the source. */
|
|
1574
|
+
function mediaTypePointer(content, contentType, fallback) {
|
|
1575
|
+
const entry = content?.[contentType];
|
|
1576
|
+
return isReference(entry) ? entry.$ref : fallback;
|
|
1369
1577
|
}
|
|
1370
1578
|
/**
|
|
1371
|
-
* Builds an `Operation` for every supported HTTP method
|
|
1372
|
-
* `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
1579
|
+
* Builds an `Operation` for every supported HTTP method and every OpenAPI 3.2 `additionalOperations` entry on
|
|
1580
|
+
* every path, in document order. `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
1373
1581
|
*/
|
|
1374
1582
|
function getOperations(document, refs) {
|
|
1375
1583
|
const operations = [];
|
|
@@ -1391,6 +1599,12 @@ function getOperations(document, refs) {
|
|
|
1391
1599
|
pathItem
|
|
1392
1600
|
});
|
|
1393
1601
|
}
|
|
1602
|
+
for (const [method, schema] of Object.entries(pathItem.additionalOperations ?? {})) if (schema && typeof schema === "object" && !SUPPORTED_METHODS.has(method.toLowerCase())) operations.push({
|
|
1603
|
+
path,
|
|
1604
|
+
method,
|
|
1605
|
+
schema,
|
|
1606
|
+
pathItem
|
|
1607
|
+
});
|
|
1394
1608
|
}
|
|
1395
1609
|
return operations;
|
|
1396
1610
|
}
|
|
@@ -1404,10 +1618,15 @@ function collectPropertyKeysByFlag(schema, flag) {
|
|
|
1404
1618
|
}
|
|
1405
1619
|
return keys.length ? keys : null;
|
|
1406
1620
|
}
|
|
1407
|
-
|
|
1621
|
+
/** Converts one parameter into a `ParameterNode`, naming an inline schema after the operation and the parameter. */
|
|
1622
|
+
function parseParameter({ param, pointer, parentName, options, parseSchema, refs }) {
|
|
1408
1623
|
const schemaName = parentName && param.name ? pascalCase(`${parentName} ${param.name}`) : void 0;
|
|
1409
1624
|
const contentType = param.schema ? void 0 : Object.keys(param.content ?? {})[0];
|
|
1410
|
-
const rawSchema = param.schema ?? (
|
|
1625
|
+
const rawSchema = param.schema ?? getMediaType({
|
|
1626
|
+
content: param.content,
|
|
1627
|
+
contentType,
|
|
1628
|
+
refs
|
|
1629
|
+
})?.schema;
|
|
1411
1630
|
const schema = rawSchema ? parseSchema({
|
|
1412
1631
|
schema: rawSchema,
|
|
1413
1632
|
name: schemaName
|
|
@@ -1430,16 +1649,17 @@ function parseParameter({ param, pointer, parentName, options, parseSchema }) {
|
|
|
1430
1649
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1431
1650
|
*/
|
|
1432
1651
|
function parseOperation({ operation, ...ctx }) {
|
|
1433
|
-
const { refs, contentType, options, parseSchema } = ctx;
|
|
1652
|
+
const { document, refs, contentType, options, parseSchema } = ctx;
|
|
1434
1653
|
const operationId = getOperationId(operation);
|
|
1435
1654
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1436
|
-
const pointer =
|
|
1655
|
+
const pointer = getOperationPointer(operation);
|
|
1437
1656
|
const parameters = getParameters({
|
|
1438
1657
|
operation,
|
|
1439
1658
|
refs
|
|
1440
1659
|
}).map((entry) => parseParameter({
|
|
1441
1660
|
options,
|
|
1442
1661
|
parseSchema,
|
|
1662
|
+
refs,
|
|
1443
1663
|
...entry,
|
|
1444
1664
|
parentName: operationName
|
|
1445
1665
|
}));
|
|
@@ -1453,20 +1673,33 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1453
1673
|
const schema = getBodySchema({
|
|
1454
1674
|
content: body?.content,
|
|
1455
1675
|
contentType: ct,
|
|
1456
|
-
refs
|
|
1676
|
+
refs,
|
|
1677
|
+
document
|
|
1457
1678
|
});
|
|
1458
1679
|
if (!schema) return [];
|
|
1459
|
-
const
|
|
1680
|
+
const mediaType = getMediaType({
|
|
1681
|
+
content: body?.content,
|
|
1682
|
+
contentType: ct,
|
|
1683
|
+
refs
|
|
1684
|
+
});
|
|
1685
|
+
const node = ast.optionality(parseSchema({
|
|
1460
1686
|
schema,
|
|
1461
1687
|
name: requestBodyName
|
|
1462
|
-
});
|
|
1463
|
-
scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}
|
|
1464
|
-
const encoding = getEncoding(
|
|
1688
|
+
}), bodyRequired);
|
|
1689
|
+
scanSchema(node, `${mediaTypePointer(body?.content, ct, `${pointer}/requestBody/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
|
|
1690
|
+
const encoding = getEncoding(mediaType?.encoding);
|
|
1465
1691
|
return [ast.factory.createContent({
|
|
1466
1692
|
contentType: ct,
|
|
1467
|
-
schema:
|
|
1693
|
+
schema: node,
|
|
1468
1694
|
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
|
|
1469
|
-
...encoding && { encoding }
|
|
1695
|
+
...encoding && { encoding },
|
|
1696
|
+
...getStreamField({
|
|
1697
|
+
contentType: ct,
|
|
1698
|
+
mediaType,
|
|
1699
|
+
refs,
|
|
1700
|
+
document,
|
|
1701
|
+
node
|
|
1702
|
+
})
|
|
1470
1703
|
})];
|
|
1471
1704
|
});
|
|
1472
1705
|
const requestBody = content.length > 0 || body?.description ? {
|
|
@@ -1484,6 +1717,12 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1484
1717
|
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
1485
1718
|
const parseEntrySchema = (ct) => {
|
|
1486
1719
|
const raw = getBodySchema({
|
|
1720
|
+
content: response?.content,
|
|
1721
|
+
contentType: ct,
|
|
1722
|
+
refs,
|
|
1723
|
+
document
|
|
1724
|
+
});
|
|
1725
|
+
const mediaType = getMediaType({
|
|
1487
1726
|
content: response?.content,
|
|
1488
1727
|
contentType: ct,
|
|
1489
1728
|
refs
|
|
@@ -1492,10 +1731,17 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1492
1731
|
schema: raw,
|
|
1493
1732
|
name: responseName
|
|
1494
1733
|
}) : ast.factory.createSchema({ type: options.emptySchemaType });
|
|
1495
|
-
if (raw && ct) scanSchema(node, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}
|
|
1734
|
+
if (raw && ct) scanSchema(node, `${mediaTypePointer(response?.content, ct, `${pointer}/responses/${statusCode}/content/${escapePointerToken(ct)}`)}/${schemaKeyword(mediaType)}`);
|
|
1496
1735
|
return {
|
|
1497
1736
|
schema: node,
|
|
1498
|
-
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1737
|
+
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly"),
|
|
1738
|
+
...ct ? getStreamField({
|
|
1739
|
+
contentType: ct,
|
|
1740
|
+
mediaType,
|
|
1741
|
+
refs,
|
|
1742
|
+
document,
|
|
1743
|
+
node
|
|
1744
|
+
}) : {}
|
|
1499
1745
|
};
|
|
1500
1746
|
};
|
|
1501
1747
|
const responseContent = (contentType ? [contentType] : Object.keys(response?.content ?? {})).map((ct) => ast.factory.createContent({
|
|
@@ -1526,7 +1772,7 @@ function parseOperation({ operation, ...ctx }) {
|
|
|
1526
1772
|
pointer,
|
|
1527
1773
|
operationId,
|
|
1528
1774
|
protocol: "http",
|
|
1529
|
-
method: operation.method.toUpperCase(),
|
|
1775
|
+
method: SUPPORTED_METHODS.has(operation.method) ? operation.method.toUpperCase() : operation.method,
|
|
1530
1776
|
path: operation.path,
|
|
1531
1777
|
tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
|
|
1532
1778
|
summary: pickDoc("summary") || void 0,
|
|
@@ -1621,6 +1867,14 @@ function createDiscriminantNode({ propertyName, values }) {
|
|
|
1621
1867
|
});
|
|
1622
1868
|
}
|
|
1623
1869
|
/**
|
|
1870
|
+
* Returns the `$ref` of an OpenAPI 3.2 `defaultMapping`, written as a schema name or a `$ref`; `undefined` when none.
|
|
1871
|
+
*/
|
|
1872
|
+
function getDefaultMappingRef(discriminator) {
|
|
1873
|
+
const value = discriminator?.defaultMapping;
|
|
1874
|
+
if (!value) return void 0;
|
|
1875
|
+
return value.includes("/") ? value : `${SCHEMA_REF_PREFIX}${value}`;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1624
1878
|
* Returns every discriminator key whose mapping value matches `ref`, in mapping order.
|
|
1625
1879
|
*
|
|
1626
1880
|
* A mapping may point several keys at the same schema, in which case the member has to be
|
|
@@ -1716,13 +1970,15 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
|
|
|
1716
1970
|
}
|
|
1717
1971
|
return constrains(variant) ? null : value;
|
|
1718
1972
|
}
|
|
1973
|
+
const defaultRef = getDefaultMappingRef(discriminator);
|
|
1719
1974
|
return unionMembers.map((s) => {
|
|
1720
1975
|
const ref = isReference(s) ? s.$ref : void 0;
|
|
1721
|
-
const
|
|
1976
|
+
const isDefault = ref !== void 0 && ref === defaultRef;
|
|
1977
|
+
const mappedValues = isDefault ? [] : findDiscriminators({
|
|
1722
1978
|
mapping: discriminator?.mapping,
|
|
1723
1979
|
ref
|
|
1724
1980
|
});
|
|
1725
|
-
const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
|
|
1981
|
+
const implicitValue = mappedValues.length || isDefault ? null : implicitDiscriminantValue(s);
|
|
1726
1982
|
const propertyType = propertyTypeOf({
|
|
1727
1983
|
schema: s,
|
|
1728
1984
|
propertyName: discriminator?.propertyName ?? "",
|
|
@@ -1767,7 +2023,8 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1767
2023
|
const parentUnion = deref?.oneOf ?? deref?.anyOf;
|
|
1768
2024
|
if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
|
|
1769
2025
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1770
|
-
const
|
|
2026
|
+
const isDefault = childRef === getDefaultMappingRef(discriminator);
|
|
2027
|
+
const values = isDefault ? [] : findDiscriminators({
|
|
1771
2028
|
mapping: discriminator.mapping,
|
|
1772
2029
|
ref: childRef
|
|
1773
2030
|
});
|
|
@@ -1790,7 +2047,7 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
|
1790
2047
|
})
|
|
1791
2048
|
});
|
|
1792
2049
|
}
|
|
1793
|
-
return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
2050
|
+
return !(parentUnion && (isDefault || values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
|
|
1794
2051
|
}),
|
|
1795
2052
|
discriminantValues
|
|
1796
2053
|
};
|
|
@@ -2112,10 +2369,12 @@ function convertUnion(context) {
|
|
|
2112
2369
|
nullable,
|
|
2113
2370
|
defaultValue
|
|
2114
2371
|
};
|
|
2115
|
-
const unionMembers = resolveUnionMembers([...schema.oneOf ?? [], ...schema.anyOf ?? []], schema.properties ?? {});
|
|
2116
|
-
const strategy = schema.oneOf ? "one" : "any";
|
|
2117
|
-
const explicitDiscriminatorPropertyName = isDiscriminator(schema) ? schema.discriminator.propertyName : void 0;
|
|
2118
2372
|
const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
2373
|
+
const declaredMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
2374
|
+
const defaultRef = getDefaultMappingRef(discriminator);
|
|
2375
|
+
const unionMembers = resolveUnionMembers(defaultRef && !declaredMembers.some((member) => isReference(member) && member.$ref === defaultRef) ? [...declaredMembers, { $ref: defaultRef }] : declaredMembers, schema.properties ?? {});
|
|
2376
|
+
const strategy = schema.oneOf ? "one" : "any";
|
|
2377
|
+
const explicitDiscriminatorPropertyName = discriminator?.propertyName;
|
|
2119
2378
|
const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
|
|
2120
2379
|
const sharedPropertiesNode = schema.properties ? parse({
|
|
2121
2380
|
schema: memberBaseSchema,
|
|
@@ -2168,7 +2427,7 @@ function convertUnion(context) {
|
|
|
2168
2427
|
members,
|
|
2169
2428
|
discriminatorPropertyName: explicitDiscriminatorPropertyName ?? ast.inferDiscriminatorPropertyName(members)
|
|
2170
2429
|
});
|
|
2171
|
-
return ast.applyMacros(unionNode, [
|
|
2430
|
+
return ast.applyMacros(unionNode, [macroDedupeUnion], { depth: "shallow" });
|
|
2172
2431
|
}
|
|
2173
2432
|
/**
|
|
2174
2433
|
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
@@ -2233,6 +2492,34 @@ function getNumericConstraints(schema) {
|
|
|
2233
2492
|
};
|
|
2234
2493
|
}
|
|
2235
2494
|
/**
|
|
2495
|
+
* Whether an enum value fits the schema's numeric bounds.
|
|
2496
|
+
*/
|
|
2497
|
+
function isWithinBounds({ value, schema }) {
|
|
2498
|
+
if (typeof value !== "number") return true;
|
|
2499
|
+
const { min, max, exclusiveMinimum, exclusiveMaximum, multipleOf } = getNumericConstraints(schema);
|
|
2500
|
+
if (min !== void 0 && value < min) return false;
|
|
2501
|
+
if (max !== void 0 && value > max) return false;
|
|
2502
|
+
if (exclusiveMinimum !== void 0 && value <= exclusiveMinimum) return false;
|
|
2503
|
+
if (exclusiveMaximum !== void 0 && value >= exclusiveMaximum) return false;
|
|
2504
|
+
if (multipleOf !== void 0 && Math.abs(value / multipleOf - Math.round(value / multipleOf)) > 1e-9) return false;
|
|
2505
|
+
return true;
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Builds the test for which `enum` entries stay: not `null`, and within the numeric bounds. `enum` and the bounds
|
|
2509
|
+
* apply together, so a value outside them is never valid. Bounds that exclude every value are a spec error, so
|
|
2510
|
+
* they are ignored rather than leaving an empty enum.
|
|
2511
|
+
*/
|
|
2512
|
+
function createEnumMemberFilter(schema) {
|
|
2513
|
+
const boundsApply = schema.enum.some((value) => value !== null && isWithinBounds({
|
|
2514
|
+
value,
|
|
2515
|
+
schema
|
|
2516
|
+
}));
|
|
2517
|
+
return (value) => value !== null && (!boundsApply || isWithinBounds({
|
|
2518
|
+
value,
|
|
2519
|
+
schema
|
|
2520
|
+
}));
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2236
2523
|
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
2237
2524
|
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
2238
2525
|
*/
|
|
@@ -2313,7 +2600,8 @@ function convertEnum({ schema, name, nullable, type, parse }) {
|
|
|
2313
2600
|
name
|
|
2314
2601
|
});
|
|
2315
2602
|
const nullInEnum = schema.enum.includes(null);
|
|
2316
|
-
const
|
|
2603
|
+
const isMember = createEnumMemberFilter(schema);
|
|
2604
|
+
const filteredValues = schema.enum.filter(isMember);
|
|
2317
2605
|
if (nullInEnum && filteredValues.length === 0) return createNullNode(schema, name);
|
|
2318
2606
|
const enumNullable = nullable || nullInEnum || void 0;
|
|
2319
2607
|
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
@@ -2332,8 +2620,9 @@ function convertEnum({ schema, name, nullable, type, parse }) {
|
|
|
2332
2620
|
primitive,
|
|
2333
2621
|
enumValues: uniqueValues
|
|
2334
2622
|
});
|
|
2335
|
-
const
|
|
2336
|
-
const
|
|
2623
|
+
const memberSlots = (list) => Array.isArray(list) ? list.filter((_, index) => isMember(schema.enum[index])) : void 0;
|
|
2624
|
+
const rawEnumNames = extensionKey ? memberSlots(schema[extensionKey]) : void 0;
|
|
2625
|
+
const rawEnumDescriptions = descriptionKey ? memberSlots(schema[descriptionKey]) : void 0;
|
|
2337
2626
|
const seenNames = /* @__PURE__ */ new Set();
|
|
2338
2627
|
return createNode(ctx, {
|
|
2339
2628
|
type: "enum",
|
|
@@ -2351,6 +2640,25 @@ function convertEnum({ schema, name, nullable, type, parse }) {
|
|
|
2351
2640
|
});
|
|
2352
2641
|
}
|
|
2353
2642
|
/**
|
|
2643
|
+
* Converts an `enum` with a handled `format` into a union of its literal values. A named enum node would rename
|
|
2644
|
+
* the exported type (`Status` becomes `StatusKey`), so the union keeps the name the format type had.
|
|
2645
|
+
*/
|
|
2646
|
+
function convertFormattedEnum(context) {
|
|
2647
|
+
const { schema, nullable } = context;
|
|
2648
|
+
const values = [...new Set(schema.enum.filter(createEnumMemberFilter(schema)))];
|
|
2649
|
+
return createNode({
|
|
2650
|
+
...context,
|
|
2651
|
+
nullable: nullable || schema.enum.includes(null) || void 0
|
|
2652
|
+
}, {
|
|
2653
|
+
type: "union",
|
|
2654
|
+
members: values.map((value) => ast.factory.createSchema({
|
|
2655
|
+
type: "enum",
|
|
2656
|
+
primitive: getPrimitiveType(typeof value),
|
|
2657
|
+
enumValues: [value]
|
|
2658
|
+
}))
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2354
2662
|
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
2355
2663
|
*/
|
|
2356
2664
|
function convertString(context) {
|
|
@@ -2527,6 +2835,24 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
|
|
|
2527
2835
|
}
|
|
2528
2836
|
// src/emit/parseSchema.ts
|
|
2529
2837
|
/**
|
|
2838
|
+
* Whether the `format` rule owns the schema: a handled format, and for a date format a `dateType` that is not `false`.
|
|
2839
|
+
*/
|
|
2840
|
+
function matchesFormat({ schema, options }) {
|
|
2841
|
+
if (!schema.format) return false;
|
|
2842
|
+
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return resolveDateTypeValue(options.dateType, schema.format) !== false;
|
|
2843
|
+
return isHandledFormat(schema.format);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Whether the format prints as a plain `string` or `number`, so narrowing it to literal values cannot break callers.
|
|
2847
|
+
* `Date`, `bigint` and `Blob` outputs keep the format.
|
|
2848
|
+
*/
|
|
2849
|
+
function formatPrintsPlainType({ schema, options, type }) {
|
|
2850
|
+
const format = schema.format;
|
|
2851
|
+
if (format === "date-time" || format === "date" || format === "time") return resolveDateTypeValue(options.dateType, format) !== "date";
|
|
2852
|
+
if ((format === "int64" || format === "uint64") && type !== "string") return options.integerType !== "bigint";
|
|
2853
|
+
return formatMap[format] !== "blob";
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2530
2856
|
* Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
|
|
2531
2857
|
* `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
|
|
2532
2858
|
* `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
|
|
@@ -2564,11 +2890,11 @@ const schemaRules = [
|
|
|
2564
2890
|
convert: convertMultiType
|
|
2565
2891
|
},
|
|
2566
2892
|
{
|
|
2567
|
-
match: (
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2893
|
+
match: (ctx) => ctx.type !== "array" && !!ctx.schema.enum?.some((value) => value !== null) && matchesFormat(ctx) && formatPrintsPlainType(ctx),
|
|
2894
|
+
convert: convertFormattedEnum
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
match: matchesFormat,
|
|
2572
2898
|
convert: convertFormat
|
|
2573
2899
|
},
|
|
2574
2900
|
{
|
|
@@ -2578,6 +2904,10 @@ const schemaRules = [
|
|
|
2578
2904
|
primitive: "string"
|
|
2579
2905
|
})
|
|
2580
2906
|
},
|
|
2907
|
+
{
|
|
2908
|
+
match: ({ schema, type }) => !type && !schema.enum?.length && (schema.contentMediaType !== void 0 || schema.contentSchema !== void 0),
|
|
2909
|
+
convert: convertString
|
|
2910
|
+
},
|
|
2581
2911
|
{
|
|
2582
2912
|
match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
|
|
2583
2913
|
convert: convertString
|
|
@@ -2872,7 +3202,7 @@ const locate = (source, location) => {
|
|
|
2872
3202
|
*/
|
|
2873
3203
|
const adapterOasName = "oas";
|
|
2874
3204
|
/**
|
|
2875
|
-
* Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.
|
|
3205
|
+
* Default Kubb adapter for OpenAPI 2.0, 3.0, 3.1, and 3.2 specifications. Reads the
|
|
2876
3206
|
* spec from `input` (a file path, URL, inline content, or parsed object), validates
|
|
2877
3207
|
* it, resolves the base URL, and converts every schema and operation into the
|
|
2878
3208
|
* universal AST that every downstream plugin consumes.
|
|
@@ -2946,6 +3276,7 @@ const adapterOas = defineExtension({
|
|
|
2946
3276
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2947
3277
|
const operationNodes = getOperations(document, refs).map((operation) => parseOperation({
|
|
2948
3278
|
operation,
|
|
3279
|
+
document,
|
|
2949
3280
|
refs,
|
|
2950
3281
|
contentType,
|
|
2951
3282
|
options: parserOptions,
|