@kubb/adapter-oas 5.0.0-alpha.43 → 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
@@ -27,6 +27,17 @@ const DEFAULT_PARSER_OPTIONS = {
27
27
  enumSuffix: "enum"
28
28
  };
29
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
+ /**
30
41
  * OpenAPI version string written into the stub document created during multi-spec merges.
31
42
  */
32
43
  const MERGE_OPENAPI_VERSION = "3.0.0";
@@ -748,12 +759,11 @@ function getResponseBody(responseBody, contentType) {
748
759
  }
749
760
  let availableContentType;
750
761
  const contentTypes = Object.keys(body.content);
751
- contentTypes.forEach((mt) => {
752
- if (!availableContentType && matchesMimeType.json(mt)) availableContentType = mt;
753
- });
754
- if (!availableContentType) contentTypes.forEach((mt) => {
755
- if (!availableContentType) availableContentType = mt;
756
- });
762
+ for (const mt of contentTypes) if (matchesMimeType.json(mt)) {
763
+ availableContentType = mt;
764
+ break;
765
+ }
766
+ if (!availableContentType) availableContentType = contentTypes[0];
757
767
  if (availableContentType) return [
758
768
  availableContentType,
759
769
  body.content[availableContentType],
@@ -773,11 +783,13 @@ function getResponseBody(responseBody, contentType) {
773
783
  * ```
774
784
  */
775
785
  function getResponseSchema(document, operation, statusCode, options = {}) {
776
- if (operation.schema.responses) Object.keys(operation.schema.responses).forEach((key) => {
777
- const schema = operation.schema.responses[key];
778
- const $ref = isReference(schema) ? schema.$ref : void 0;
779
- if (schema && $ref) operation.schema.responses[key] = resolveRef(document, $ref);
780
- });
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
+ }
781
793
  const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
782
794
  if (responseBody === false) return {};
783
795
  const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema;
@@ -816,14 +828,24 @@ function getRequestSchema(document, operation, options = {}) {
816
828
  * // returned unchanged — contains a $ref
817
829
  * ```
818
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
+ }
819
841
  function flattenSchema(schema) {
820
842
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
821
- if (schema.allOf.some((item) => isRef(item))) return schema;
822
- const isPlainFragment = (item) => !Object.keys(item).some((key) => structuralKeys.has(key));
823
- 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;
824
846
  const merged = { ...schema };
825
847
  delete merged.allOf;
826
- 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;
827
849
  return merged;
828
850
  }
829
851
  /**
@@ -853,10 +875,15 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
853
875
  for (const item of schema) collectRefs(item, refs);
854
876
  return refs;
855
877
  }
856
- if (schema && typeof schema === "object") for (const [key, value] of Object.entries(schema)) if (key === "$ref" && typeof value === "string") {
857
- const match = value.match(/^#\/components\/schemas\/(.+)$/);
858
- if (match) refs.add(match[1]);
859
- } 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
+ }
860
887
  return refs;
861
888
  }
862
889
  /**
@@ -940,13 +967,23 @@ function getSchemas(document, { contentType }) {
940
967
  }
941
968
  const schemas = {};
942
969
  const nameMapping = /* @__PURE__ */ new Map();
943
- const multipleSources = (items) => new Set(items.map((i) => i.source)).size > 1;
944
- for (const [, items] of normalizedNames) items.forEach((item, index) => {
945
- const suffix = items.length === 1 ? "" : multipleSources(items) ? getSemanticSuffix(item.source) : index === 0 ? "" : String(index + 1);
946
- const uniqueName = item.originalName + suffix;
947
- schemas[uniqueName] = item.schema;
948
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
949
- });
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
+ }
950
987
  return {
951
988
  schemas: sortSchemas(schemas),
952
989
  nameMapping
@@ -1093,7 +1130,7 @@ function createSchemaParser(ctx) {
1093
1130
  if (!deref || !isDiscriminator(deref)) return true;
1094
1131
  const parentUnion = deref.oneOf ?? deref.anyOf;
1095
1132
  if (!parentUnion) return true;
1096
- const childRef = `#/components/schemas/${name}`;
1133
+ const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1097
1134
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1098
1135
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1099
1136
  if (inOneOf || inMapping) {
@@ -1588,41 +1625,70 @@ function createSchemaParser(ctx) {
1588
1625
  });
1589
1626
  }
1590
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
+ /**
1591
1666
  * Converts an OAS `Operation` into an `OperationNode`.
1592
1667
  */
1593
1668
  function parseOperation(options, operation) {
1594
1669
  const parameters = getParameters(document, operation).map((param) => parseParameter(options, param));
1595
1670
  const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType });
1596
1671
  const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : void 0;
1597
- const requestBodyDescription = operation.schema.requestBody && !isReference(operation.schema.requestBody) ? operation.schema.requestBody.description : void 0;
1598
- const requestBodyKeysToOmit = requestBodySchema?.properties ? Object.entries(requestBodySchema.properties).filter(([, prop]) => !isReference(prop) && prop.readOnly).map(([key]) => key) : void 0;
1599
- const requestBodyRequired = operation.schema.requestBody && !isReference(operation.schema.requestBody) ? operation.schema.requestBody.required === true : false;
1600
- const requestBodyContentType = (() => {
1601
- if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) return;
1602
- const content = operation.schema.requestBody.content;
1603
- return content ? Object.keys(content)[0] : void 0;
1604
- })();
1672
+ const requestBodyMeta = getRequestBodyMeta(operation);
1605
1673
  const requestBody = requestBodySchemaNode ? {
1606
- description: requestBodyDescription,
1607
- schema: ast.syncOptionality(requestBodySchemaNode, requestBodyRequired),
1608
- keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : void 0,
1609
- required: requestBodyRequired || void 0,
1610
- 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
1611
1679
  } : void 0;
1612
1680
  const responses = operation.getResponseStatusCodes().map((statusCode) => {
1613
1681
  const responseObj = operation.getResponseByStatusCode(statusCode);
1614
1682
  const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType });
1615
1683
  const schema = responseSchema && Object.keys(responseSchema).length > 0 ? parseSchema({ schema: responseSchema }, options) : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType) });
1616
- const description = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : void 0;
1617
- const rawContent = typeof responseObj === "object" && responseObj !== null && !Array.isArray(responseObj) ? responseObj.content : void 0;
1618
- const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? "") : getMediaType(operation.contentType ?? "");
1619
- 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 ?? "");
1620
1686
  return ast.createResponse({
1621
1687
  statusCode,
1622
1688
  description,
1623
1689
  schema,
1624
1690
  mediaType,
1625
- keysToOmit: keysToOmit?.length ? keysToOmit : void 0
1691
+ keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
1626
1692
  });
1627
1693
  });
1628
1694
  const urlPath = new URLPath(operation.path);