@kubb/adapter-oas 5.0.0-alpha.42 → 5.0.0-alpha.44
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/dist/index.cjs +161 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +332 -13
- package/dist/index.js +159 -60
- package/dist/index.js.map +1 -1
- package/package.json +6 -7
- package/src/constants.ts +12 -0
- package/src/factory.ts +8 -12
- package/src/guards.ts +1 -1
- package/src/index.ts +2 -2
- package/src/parser.ts +68 -54
- package/src/refs.ts +4 -2
- package/src/resolvers.ts +48 -25
package/dist/index.js
CHANGED
|
@@ -2,12 +2,9 @@ import "./chunk--u3MIqq1.js";
|
|
|
2
2
|
import { ast, createAdapter } from "@kubb/core";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { bundle, loadConfig } from "@redocly/openapi-core";
|
|
5
|
-
import yaml from "@stoplight/yaml";
|
|
6
5
|
import OASNormalize from "oas-normalize";
|
|
7
|
-
import { isPlainObject, mergeDeep } from "remeda";
|
|
8
6
|
import swagger2openapi from "swagger2openapi";
|
|
9
7
|
import BaseOas from "oas";
|
|
10
|
-
import jsonpointer from "jsonpointer";
|
|
11
8
|
import { isRef } from "oas/types";
|
|
12
9
|
import { matchesMimeType } from "oas/utils";
|
|
13
10
|
//#region src/constants.ts
|
|
@@ -30,6 +27,17 @@ const DEFAULT_PARSER_OPTIONS = {
|
|
|
30
27
|
enumSuffix: "enum"
|
|
31
28
|
};
|
|
32
29
|
/**
|
|
30
|
+
* JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
|
|
31
|
+
*
|
|
32
|
+
* Used when building or parsing `$ref` strings.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
const SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
40
|
+
/**
|
|
33
41
|
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
34
42
|
*/
|
|
35
43
|
const MERGE_OPENAPI_VERSION = "3.0.0";
|
|
@@ -258,6 +266,40 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
|
258
266
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
259
267
|
}
|
|
260
268
|
//#endregion
|
|
269
|
+
//#region ../../internals/utils/src/object.ts
|
|
270
|
+
/**
|
|
271
|
+
* Returns `true` when `value` is a plain (non-null, non-array) object.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* isPlainObject({}) // true
|
|
276
|
+
* isPlainObject([]) // false
|
|
277
|
+
* isPlainObject(null) // false
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
function isPlainObject(value) {
|
|
281
|
+
return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Recursively merges `source` into `target`, combining nested plain objects.
|
|
285
|
+
* Arrays and non-object values from `source` override the corresponding values in `target`.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
|
|
290
|
+
* // { a: { x: 1, y: 2 } }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
function mergeDeep(target, source) {
|
|
294
|
+
const result = { ...target };
|
|
295
|
+
for (const key of Object.keys(source)) {
|
|
296
|
+
const sv = source[key];
|
|
297
|
+
const tv = result[key];
|
|
298
|
+
result[key] = sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv) ? mergeDeep(tv, sv) : sv;
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
261
303
|
//#region ../../internals/utils/src/reserved.ts
|
|
262
304
|
/**
|
|
263
305
|
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
@@ -520,7 +562,7 @@ async function parseDocument(pathOrApi, { canBundle = true, enablePaths = true }
|
|
|
520
562
|
/**
|
|
521
563
|
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
522
564
|
*
|
|
523
|
-
* Each document is parsed independently then recursively merged
|
|
565
|
+
* Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
|
|
524
566
|
* Throws when the input array is empty.
|
|
525
567
|
*
|
|
526
568
|
* @example
|
|
@@ -563,11 +605,7 @@ async function mergeDocuments(pathOrApi) {
|
|
|
563
605
|
function parseFromConfig(source) {
|
|
564
606
|
if (source.type === "data") {
|
|
565
607
|
if (typeof source.data === "object") return parseDocument(structuredClone(source.data));
|
|
566
|
-
|
|
567
|
-
return parseDocument(yaml.parse(source.data));
|
|
568
|
-
} catch {
|
|
569
|
-
return parseDocument(source.data);
|
|
570
|
-
}
|
|
608
|
+
return parseDocument(source.data, { canBundle: false });
|
|
571
609
|
}
|
|
572
610
|
if (source.type === "paths") return mergeDocuments(source.paths);
|
|
573
611
|
if (new URLPath(source.path).isURL) return parseDocument(source.path);
|
|
@@ -610,7 +648,7 @@ function resolveRef(document, $ref) {
|
|
|
610
648
|
if ($ref === "") return null;
|
|
611
649
|
if ($ref.startsWith("#")) $ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
612
650
|
else return null;
|
|
613
|
-
const current =
|
|
651
|
+
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
614
652
|
if (!current) throw new Error(`Could not find a definition for ${origRef}.`);
|
|
615
653
|
return current;
|
|
616
654
|
}
|
|
@@ -721,12 +759,11 @@ function getResponseBody(responseBody, contentType) {
|
|
|
721
759
|
}
|
|
722
760
|
let availableContentType;
|
|
723
761
|
const contentTypes = Object.keys(body.content);
|
|
724
|
-
contentTypes.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
});
|
|
762
|
+
for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
|
|
763
|
+
availableContentType = mt;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
if (!availableContentType) availableContentType = contentTypes[0];
|
|
730
767
|
if (availableContentType) return [
|
|
731
768
|
availableContentType,
|
|
732
769
|
body.content[availableContentType],
|
|
@@ -746,11 +783,13 @@ function getResponseBody(responseBody, contentType) {
|
|
|
746
783
|
* ```
|
|
747
784
|
*/
|
|
748
785
|
function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
749
|
-
if (operation.schema.responses)
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
786
|
+
if (operation.schema.responses) {
|
|
787
|
+
const responses = operation.schema.responses;
|
|
788
|
+
for (const key in responses) {
|
|
789
|
+
const schema = responses[key];
|
|
790
|
+
if (schema && isReference(schema)) responses[key] = resolveRef(document, schema.$ref);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
754
793
|
const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
|
|
755
794
|
if (responseBody === false) return {};
|
|
756
795
|
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
|
|
@@ -789,14 +828,24 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
789
828
|
* // returned unchanged — contains a $ref
|
|
790
829
|
* ```
|
|
791
830
|
*/
|
|
831
|
+
/**
|
|
832
|
+
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
833
|
+
* structurally significant on its own (see `structuralKeys`).
|
|
834
|
+
*
|
|
835
|
+
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
836
|
+
*/
|
|
837
|
+
function hasStructuralKeywords(fragment) {
|
|
838
|
+
for (const key in fragment) if (structuralKeys.has(key)) return true;
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
792
841
|
function flattenSchema(schema) {
|
|
793
842
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (
|
|
843
|
+
const allOfFragments = schema.allOf;
|
|
844
|
+
if (allOfFragments.some((item) => isRef(item))) return schema;
|
|
845
|
+
if (allOfFragments.some(hasStructuralKeywords)) return schema;
|
|
797
846
|
const merged = { ...schema };
|
|
798
847
|
delete merged.allOf;
|
|
799
|
-
for (const fragment of
|
|
848
|
+
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
|
|
800
849
|
return merged;
|
|
801
850
|
}
|
|
802
851
|
/**
|
|
@@ -826,10 +875,15 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
|
|
|
826
875
|
for (const item of schema) collectRefs(item, refs);
|
|
827
876
|
return refs;
|
|
828
877
|
}
|
|
829
|
-
if (schema && typeof schema === "object") for (const
|
|
830
|
-
const
|
|
831
|
-
if (
|
|
832
|
-
|
|
878
|
+
if (schema && typeof schema === "object") for (const key in schema) {
|
|
879
|
+
const value = schema[key];
|
|
880
|
+
if (key === "$ref" && typeof value === "string") {
|
|
881
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
882
|
+
const name = value.slice(21);
|
|
883
|
+
if (name) refs.add(name);
|
|
884
|
+
}
|
|
885
|
+
} else collectRefs(value, refs);
|
|
886
|
+
}
|
|
833
887
|
return refs;
|
|
834
888
|
}
|
|
835
889
|
/**
|
|
@@ -913,13 +967,23 @@ function getSchemas(document, { contentType }) {
|
|
|
913
967
|
}
|
|
914
968
|
const schemas = {};
|
|
915
969
|
const nameMapping = /* @__PURE__ */ new Map();
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
970
|
+
for (const [, items] of normalizedNames) {
|
|
971
|
+
const isSingle = items.length === 1;
|
|
972
|
+
let hasMultipleSources = false;
|
|
973
|
+
if (!isSingle) {
|
|
974
|
+
const firstSource = items[0].source;
|
|
975
|
+
for (let i = 1; i < items.length; i++) if (items[i].source !== firstSource) {
|
|
976
|
+
hasMultipleSources = true;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
items.forEach((item, index) => {
|
|
981
|
+
const suffix = isSingle ? "" : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
|
|
982
|
+
const uniqueName = item.originalName + suffix;
|
|
983
|
+
schemas[uniqueName] = item.schema;
|
|
984
|
+
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
923
987
|
return {
|
|
924
988
|
schemas: sortSchemas(schemas),
|
|
925
989
|
nameMapping
|
|
@@ -1066,7 +1130,7 @@ function createSchemaParser(ctx) {
|
|
|
1066
1130
|
if (!deref || !isDiscriminator(deref)) return true;
|
|
1067
1131
|
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1068
1132
|
if (!parentUnion) return true;
|
|
1069
|
-
const childRef =
|
|
1133
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1070
1134
|
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1071
1135
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1072
1136
|
if (inOneOf || inMapping) {
|
|
@@ -1295,15 +1359,21 @@ function createSchemaParser(ctx) {
|
|
|
1295
1359
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1296
1360
|
if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1297
1361
|
const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
|
|
1298
|
-
const
|
|
1362
|
+
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1363
|
+
const uniqueValues = [...new Set(filteredValues)];
|
|
1364
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1299
1365
|
return ast.createSchema({
|
|
1300
1366
|
...enumBase,
|
|
1301
1367
|
primitive: enumPrimitiveType,
|
|
1302
|
-
namedEnumValues:
|
|
1303
|
-
name: String(
|
|
1304
|
-
value
|
|
1368
|
+
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1369
|
+
name: String(rawEnumNames?.[index] ?? value),
|
|
1370
|
+
value,
|
|
1305
1371
|
primitive: enumPrimitiveType
|
|
1306
|
-
}))
|
|
1372
|
+
})).filter((entry) => {
|
|
1373
|
+
if (seenNames.has(entry.name)) return false;
|
|
1374
|
+
seenNames.add(entry.name);
|
|
1375
|
+
return true;
|
|
1376
|
+
})
|
|
1307
1377
|
});
|
|
1308
1378
|
}
|
|
1309
1379
|
return ast.createSchema({
|
|
@@ -1555,41 +1625,70 @@ function createSchemaParser(ctx) {
|
|
|
1555
1625
|
});
|
|
1556
1626
|
}
|
|
1557
1627
|
/**
|
|
1628
|
+
* Reads the inline `requestBody` metadata (description / required / contentType) that OAS exposes
|
|
1629
|
+
* outside the schema itself. Returns an empty object when the request body is missing or a `$ref`.
|
|
1630
|
+
*/
|
|
1631
|
+
function getRequestBodyMeta(operation) {
|
|
1632
|
+
const body = operation.schema.requestBody;
|
|
1633
|
+
if (!body || isReference(body)) return { required: false };
|
|
1634
|
+
const inline = body;
|
|
1635
|
+
return {
|
|
1636
|
+
description: inline.description,
|
|
1637
|
+
required: inline.required === true,
|
|
1638
|
+
contentType: inline.content ? Object.keys(inline.content)[0] : void 0
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
|
|
1643
|
+
*/
|
|
1644
|
+
function getResponseMeta(responseObj) {
|
|
1645
|
+
if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
|
|
1646
|
+
const inline = responseObj;
|
|
1647
|
+
return {
|
|
1648
|
+
description: inline.description,
|
|
1649
|
+
content: inline.content
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
1654
|
+
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
1655
|
+
*/
|
|
1656
|
+
function collectPropertyKeysByFlag(schema, flag) {
|
|
1657
|
+
if (!schema?.properties) return void 0;
|
|
1658
|
+
const keys = [];
|
|
1659
|
+
for (const key in schema.properties) {
|
|
1660
|
+
const prop = schema.properties[key];
|
|
1661
|
+
if (prop && !isReference(prop) && prop[flag]) keys.push(key);
|
|
1662
|
+
}
|
|
1663
|
+
return keys.length ? keys : void 0;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1558
1666
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1559
1667
|
*/
|
|
1560
1668
|
function parseOperation(options, operation) {
|
|
1561
1669
|
const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
|
|
1562
1670
|
const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType });
|
|
1563
1671
|
const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : void 0;
|
|
1564
|
-
const
|
|
1565
|
-
const requestBodyKeysToOmit = requestBodySchema?.properties ? Object.entries(requestBodySchema.properties).filter(([, prop]) => !isReference(prop) && prop.readOnly).map(([key]) => key) : void 0;
|
|
1566
|
-
const requestBodyRequired = operation.schema.requestBody && !isReference(operation.schema.requestBody) ? operation.schema.requestBody.required === true : false;
|
|
1567
|
-
const requestBodyContentType = (() => {
|
|
1568
|
-
if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) return;
|
|
1569
|
-
const content = operation.schema.requestBody.content;
|
|
1570
|
-
return content ? Object.keys(content)[0] : void 0;
|
|
1571
|
-
})();
|
|
1672
|
+
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
1572
1673
|
const requestBody = requestBodySchemaNode ? {
|
|
1573
|
-
description:
|
|
1574
|
-
schema: ast.syncOptionality(requestBodySchemaNode,
|
|
1575
|
-
keysToOmit:
|
|
1576
|
-
required:
|
|
1577
|
-
contentType:
|
|
1674
|
+
description: requestBodyMeta.description,
|
|
1675
|
+
schema: ast.syncOptionality(requestBodySchemaNode, requestBodyMeta.required),
|
|
1676
|
+
keysToOmit: collectPropertyKeysByFlag(requestBodySchema, "readOnly"),
|
|
1677
|
+
required: requestBodyMeta.required || void 0,
|
|
1678
|
+
contentType: requestBodyMeta.contentType
|
|
1578
1679
|
} : void 0;
|
|
1579
1680
|
const responses = operation.getResponseStatusCodes().map((statusCode) => {
|
|
1580
1681
|
const responseObj = operation.getResponseByStatusCode(statusCode);
|
|
1581
1682
|
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
|
|
1582
1683
|
const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
|
|
1583
|
-
const description
|
|
1584
|
-
const
|
|
1585
|
-
const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? "") : getMediaType(operation.contentType ?? "");
|
|
1586
|
-
const keysToOmit = responseSchema?.properties ? Object.entries(responseSchema.properties).filter(([, prop]) => !isReference(prop) && prop.writeOnly).map(([key]) => key) : void 0;
|
|
1684
|
+
const { description, content } = getResponseMeta(responseObj);
|
|
1685
|
+
const mediaType = content ? getMediaType(Object.keys(content)[0] ?? "") : getMediaType(operation.contentType ?? "");
|
|
1587
1686
|
return ast.createResponse({
|
|
1588
1687
|
statusCode,
|
|
1589
1688
|
description,
|
|
1590
1689
|
schema,
|
|
1591
1690
|
mediaType,
|
|
1592
|
-
keysToOmit:
|
|
1691
|
+
keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
|
|
1593
1692
|
});
|
|
1594
1693
|
});
|
|
1595
1694
|
const urlPath = new URLPath(operation.path);
|