@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.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 with `remeda`'s `mergeDeep`.
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
- try {
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 = jsonpointer.get(document, $ref);
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.forEach((mt) => {
725
- if (!availableContentType && matchesMimeType.json(mt)) availableContentType = mt;
726
- });
727
- if (!availableContentType) contentTypes.forEach((mt) => {
728
- if (!availableContentType) availableContentType = mt;
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) Object.keys(operation.schema.responses).forEach((key) => {
750
- const schema = operation.schema.responses[key];
751
- const $ref = isReference(schema) ? schema.$ref : void 0;
752
- if (schema && $ref) operation.schema.responses[key] = resolveRef(document, $ref);
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
- if (schema.allOf.some((item) => isRef(item))) return schema;
795
- const isPlainFragment = (item) => !Object.keys(item).some((key) => structuralKeys.has(key));
796
- if (!schema.allOf.every((item) => isPlainFragment(item))) return schema;
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 schema.allOf) for (const [key, value] of Object.entries(fragment)) if (merged[key] === void 0) merged[key] = value;
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 [key, value] of Object.entries(schema)) if (key === "$ref" && typeof value === "string") {
830
- const match = value.match(/^#\/components\/schemas\/(.+)$/);
831
- if (match) refs.add(match[1]);
832
- } else collectRefs(value, refs);
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
- const multipleSources = (items) => new Set(items.map((i) => i.source)).size > 1;
917
- for (const [, items] of normalizedNames) items.forEach((item, index) => {
918
- const suffix = items.length === 1 ? "" : multipleSources(items) ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
919
- const uniqueName = item.originalName + suffix;
920
- schemas[uniqueName] = item.schema;
921
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
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 = `#/components/schemas/${name}`;
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 sourceValues = extensionKey ? [...new Set(schema[extensionKey])] : [...new Set(filteredValues)];
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: sourceValues.map((label, index) => ({
1303
- name: String(label),
1304
- value: extensionKey ? filteredValues[index] ?? label : label,
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 requestBodyDescription = operation.schema.requestBody && !isReference(operation.schema.requestBody) ? operation.schema.requestBody.description : void 0;
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: requestBodyDescription,
1574
- schema: ast.syncOptionality(requestBodySchemaNode, requestBodyRequired),
1575
- keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : void 0,
1576
- required: requestBodyRequired || void 0,
1577
- contentType: requestBodyContentType
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 = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : void 0;
1584
- const rawContent = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.content : void 0;
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: keysToOmit?.length ? keysToOmit : void 0
1691
+ keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
1593
1692
  });
1594
1693
  });
1595
1694
  const urlPath = new URLPath(operation.path);