@kubb/adapter-oas 5.0.0-beta.62 → 5.0.0-beta.63

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.d.ts CHANGED
@@ -1250,7 +1250,7 @@ type AdapterOasResolvedOptions = {
1250
1250
  enumSuffix: AdapterOasOptions['enumSuffix'];
1251
1251
  /**
1252
1252
  * Map from original `$ref` paths to their collision-resolved schema names.
1253
- * Populated after each `parse()` call.
1253
+ * Populated once the adapter resolves a spec's schemas, on the first `stream()` or `parse()`.
1254
1254
  *
1255
1255
  * @example
1256
1256
  * ```ts
@@ -1267,7 +1267,7 @@ type AdapterOas = AdapterFactoryOptions<'oas', AdapterOasOptions, AdapterOasReso
1267
1267
  //#endregion
1268
1268
  //#region src/adapter.d.ts
1269
1269
  /**
1270
- * Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
1270
+ * The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
1271
1271
  */
1272
1272
  declare const adapterOasName = "oas";
1273
1273
  /**
package/dist/index.js CHANGED
@@ -14,10 +14,9 @@ import { collect, narrowSchema } from "@kubb/ast";
14
14
  *
15
15
  * @example
16
16
  * ```ts
17
- * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
17
+ * import { DEFAULT_PARSER_OPTIONS, parseOas } from '@kubb/adapter-oas'
18
18
  *
19
- * const parser = createOasParser(oas)
20
- * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
19
+ * const { root } = parseOas(document, { ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
21
20
  * ```
22
21
  */
23
22
  const DEFAULT_PARSER_OPTIONS = {
@@ -88,12 +87,24 @@ const structuralKeys = new Set([
88
87
  "not"
89
88
  ]);
90
89
  /**
90
+ * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
91
+ * `int64` and the date/time family. Keep this in sync with the `convertFormat`
92
+ * special-cases in `parser.ts`. `isHandledFormat` reads it so the
93
+ * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
94
+ */
95
+ const specialCasedFormats = new Set([
96
+ "int64",
97
+ "date-time",
98
+ "date",
99
+ "time"
100
+ ]);
101
+ /**
91
102
  * Static map from OAS `format` strings to Kubb `SchemaType` values.
92
103
  *
93
104
  * Only formats whose AST type differs from the OAS `type` field appear here.
94
- * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
95
- * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname` and
96
- * `idn-hostname` map to `'url'` as the closest generic string-format type.
105
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
106
+ * separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
107
+ * and `idn-hostname` map to `'url'` as the closest generic string-format type.
97
108
  *
98
109
  * @example
99
110
  * ```ts
@@ -104,18 +115,6 @@ const structuralKeys = new Set([
104
115
  * formatMap['float'] // 'number'
105
116
  * ```
106
117
  */
107
- /**
108
- * Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
109
- * `int64` and the date/time family. Keep this in sync with the `convertFormat`
110
- * special-cases in `parser.ts`. `isHandledFormat` reads it so the
111
- * `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
112
- */
113
- const specialCasedFormats = new Set([
114
- "int64",
115
- "date-time",
116
- "date",
117
- "time"
118
- ]);
119
118
  const formatMap = {
120
119
  uuid: "uuid",
121
120
  email: "email",
@@ -636,7 +635,7 @@ function isDiscriminator(obj) {
636
635
  //#endregion
637
636
  //#region src/factory.ts
638
637
  /**
639
- * Loads and dereferences an OpenAPI document, returning the raw `Document`.
638
+ * Loads and bundles an OpenAPI document, returning the raw `Document`.
640
639
  *
641
640
  * Accepts a file path string or an already-parsed document object. File paths and URLs are
642
641
  * bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
@@ -747,17 +746,180 @@ async function validateDocument(document, { throwOnError = false } = {}) {
747
746
  }
748
747
  }
749
748
  //#endregion
749
+ //#region src/dedupe.ts
750
+ /**
751
+ * Minimum occurrences before a shape is deduplicated.
752
+ */
753
+ const MIN_OCCURRENCES = 2;
754
+ /**
755
+ * Builds the shared `ref` replacement for a duplicate occurrence, carrying the
756
+ * usage-slot and documentation fields that are not part of the shared type.
757
+ */
758
+ function createRefNode(node, target) {
759
+ return ast.factory.createSchema({
760
+ type: "ref",
761
+ name: target.name,
762
+ ref: target.ref,
763
+ optional: node.optional,
764
+ nullish: node.nullish,
765
+ readOnly: node.readOnly,
766
+ writeOnly: node.writeOnly,
767
+ deprecated: node.deprecated,
768
+ description: node.description,
769
+ default: node.default,
770
+ example: node.example
771
+ });
772
+ }
773
+ /**
774
+ * Strips usage-slot flags from an extracted definition and applies its name.
775
+ * A standalone definition is never optional, so `optional`/`nullish` are cleared.
776
+ */
777
+ function cleanDefinition(node, name) {
778
+ return {
779
+ ...node,
780
+ name,
781
+ optional: void 0,
782
+ nullish: void 0
783
+ };
784
+ }
785
+ /**
786
+ * Returns `true` when a node is eligible for deduplication. Only enums and objects qualify, and
787
+ * object shapes that take part in a circular chain are rejected so recursive structures are not
788
+ * extracted (which would break the cycle).
789
+ */
790
+ function isCandidate(node, circularSchemas) {
791
+ if (node.type === "enum") return true;
792
+ if (node.type !== "object") return false;
793
+ if (node.name && circularSchemas.has(node.name)) return false;
794
+ return !containsCircularRef(node, { circularSchemas });
795
+ }
796
+ /**
797
+ * Produces the name for an inline shape with no existing named component, resolving
798
+ * collisions against `usedNames`. Returns `null` for an unnamed shape, which stays inline.
799
+ */
800
+ function nameFor(node, usedNames) {
801
+ const base = node.name;
802
+ if (!base) return null;
803
+ let name = base;
804
+ let counter = 2;
805
+ while (usedNames.has(name)) name = `${base}${counter++}`;
806
+ usedNames.add(name);
807
+ return name;
808
+ }
809
+ /**
810
+ * Scans a forest of schema and operation nodes and produces a {@link Plan}.
811
+ *
812
+ * A shape that occurs at least {@link MIN_OCCURRENCES} times is deduplicated: if any occurrence is
813
+ * a named top-level schema, the first one becomes the target (so other top-level duplicates and
814
+ * inline copies turn into references to it). Other top-level names with the same content are
815
+ * recorded as aliases. Otherwise a new definition is extracted using {@link nameFor}. The returned
816
+ * plan rewrites nodes against those decisions.
817
+ *
818
+ * @example
819
+ * ```ts
820
+ * const dedupePlan = plan([...schemaNodes, ...operationNodes], { circularSchemas, usedNames })
821
+ * ```
822
+ */
823
+ function plan(roots, context) {
824
+ const { circularSchemas, usedNames } = context;
825
+ const topLevelNodes = /* @__PURE__ */ new Set();
826
+ const groups = /* @__PURE__ */ new Map();
827
+ for (const root of roots) {
828
+ if (root.kind === "Schema") topLevelNodes.add(root);
829
+ for (const schemaNode of ast.collect(root, { schema: (node) => node })) {
830
+ if (!isCandidate(schemaNode, circularSchemas)) continue;
831
+ const signature = ast.signatureOf(schemaNode);
832
+ const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
833
+ const group = groups.get(signature);
834
+ if (group) {
835
+ group.count++;
836
+ if (isTopLevel) group.topLevelNames.push(schemaNode.name);
837
+ } else groups.set(signature, {
838
+ count: 1,
839
+ representative: schemaNode,
840
+ topLevelNames: isTopLevel ? [schemaNode.name] : []
841
+ });
842
+ }
843
+ }
844
+ const bySignature = /* @__PURE__ */ new Map();
845
+ const byName = /* @__PURE__ */ new Map();
846
+ const pendingExtractions = [];
847
+ for (const [signature, group] of groups) {
848
+ if (group.count < MIN_OCCURRENCES) continue;
849
+ const [firstName, ...duplicateNames] = group.topLevelNames;
850
+ if (firstName) {
851
+ const target = {
852
+ name: firstName,
853
+ ref: `${SCHEMA_REF_PREFIX}${firstName}`
854
+ };
855
+ bySignature.set(signature, target);
856
+ for (const duplicate of duplicateNames) byName.set(duplicate, target);
857
+ continue;
858
+ }
859
+ const name = nameFor(group.representative, usedNames);
860
+ if (!name) continue;
861
+ bySignature.set(signature, {
862
+ name,
863
+ ref: `${SCHEMA_REF_PREFIX}${name}`
864
+ });
865
+ pendingExtractions.push({
866
+ name,
867
+ representative: group.representative
868
+ });
869
+ }
870
+ function rewrite(node, skipRootMatch) {
871
+ if (bySignature.size === 0 && byName.size === 0) return node;
872
+ const root = node;
873
+ return ast.transform(node, { schema(schemaNode) {
874
+ if (schemaNode.type === "ref") {
875
+ const refName = schemaNode.ref ? extractRefName(schemaNode.ref) : schemaNode.name;
876
+ const target = refName ? byName.get(refName) : void 0;
877
+ return target ? {
878
+ ...schemaNode,
879
+ name: target.name,
880
+ ref: target.ref
881
+ } : void 0;
882
+ }
883
+ if (skipRootMatch && schemaNode === root) return void 0;
884
+ const target = bySignature.get(ast.signatureOf(schemaNode));
885
+ if (!target) return void 0;
886
+ return createRefNode(schemaNode, target);
887
+ } });
888
+ }
889
+ return {
890
+ extracted: pendingExtractions.map(({ name, representative }) => cleanDefinition(rewrite(representative, true), name)),
891
+ apply(node) {
892
+ return rewrite(node, false);
893
+ },
894
+ applyTopLevel(node) {
895
+ const target = bySignature.get(ast.signatureOf(node));
896
+ if (target && target.name !== node.name) return ast.factory.createSchema({
897
+ type: "ref",
898
+ name: node.name ?? null,
899
+ ref: target.ref,
900
+ description: node.description,
901
+ deprecated: node.deprecated
902
+ });
903
+ return rewrite(node, true);
904
+ },
905
+ isAlias(name) {
906
+ return byName.has(name);
907
+ }
908
+ };
909
+ }
910
+ //#endregion
750
911
  //#region src/refs.ts
751
912
  const _refCache = /* @__PURE__ */ new WeakMap();
752
913
  /**
753
914
  * Resolves a local JSON pointer reference from a document.
754
915
  *
755
- * Accepts `#/...` refs. Returns `null` for empty or non-local refs.
756
- * Throws when the pointer cannot be resolved.
916
+ * Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
917
+ * resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
918
+ * build there is no sink to collect it, so it throws instead.
757
919
  *
758
920
  * @example
759
921
  * ```ts
760
- * resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
922
+ * resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
761
923
  * ```
762
924
  */
763
925
  function resolveRef(document, $ref) {
@@ -814,23 +976,6 @@ function dereferenceWithRef(document, schema) {
814
976
  //#endregion
815
977
  //#region src/dialect.ts
816
978
  /**
817
- * Derives a schema's `optional`/`nullish` flags from a parent's `required` value and the
818
- * schema's own `nullable`. How "required" and "nullable" combine is OpenAPI-specific, so it
819
- * lives in the dialect.
820
- *
821
- * - Non-required + non-nullable → `optional: true`.
822
- * - Non-required + nullable → `nullish: true`.
823
- * - Required → both flags cleared.
824
- */
825
- function optionality(schema, required) {
826
- const nullable = schema.nullable ?? false;
827
- return {
828
- ...schema,
829
- optional: !required && !nullable ? true : void 0,
830
- nullish: !required && nullable ? true : void 0
831
- };
832
- }
833
- /**
834
979
  * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
835
980
  *
836
981
  * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
@@ -856,14 +1001,14 @@ const oasDialect = ast.defineDialect({
856
1001
  isReference,
857
1002
  isDiscriminator,
858
1003
  isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
859
- resolveRef,
860
- optionality
861
- }
1004
+ resolveRef
1005
+ },
1006
+ dedupe: { plan }
862
1007
  });
863
1008
  //#endregion
864
1009
  //#region src/discriminator.ts
865
1010
  /**
866
- * Builds a map of child schema names discriminator patch data by scanning the given
1011
+ * Maps each child schema name to its discriminator patch data by scanning the given
867
1012
  * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
868
1013
  *
869
1014
  * The streaming path calls this on a small pre-parsed subset of schemas (only the
@@ -979,9 +1124,8 @@ function findDiscriminator(mapping, ref) {
979
1124
  /**
980
1125
  * MIME type fragments that mark a media type as JSON-like.
981
1126
  *
982
- * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
983
- * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
984
- * `application/vnd.api+json`.
1127
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
1128
+ * structured-syntax suffixes such as `application/vnd.api+json`.
985
1129
  */
986
1130
  const jsonMimeFragments = [
987
1131
  "application/json",
@@ -1077,8 +1221,8 @@ function getRequestContent({ document, operation, mediaType }) {
1077
1221
  return available ? [available, content[available]] : false;
1078
1222
  }
1079
1223
  /**
1080
- * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
1081
- * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
1224
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins
1225
+ * when several are declared), then the first declared one, defaulting to `'application/json'`.
1082
1226
  */
1083
1227
  function getRequestContentType({ document, operation }) {
1084
1228
  const content = getRequestBodyContent({
@@ -1173,9 +1317,9 @@ function getSchemaType(format) {
1173
1317
  return formatMap[format] ?? null;
1174
1318
  }
1175
1319
  /**
1176
- * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
1177
- * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
1178
- * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
1320
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
1321
+ * `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
1322
+ * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
1179
1323
  * diagnostic in step with the parser as `formatMap` grows.
1180
1324
  */
1181
1325
  function isHandledFormat(format) {
@@ -1193,7 +1337,7 @@ function getPrimitiveType(type) {
1193
1337
  /**
1194
1338
  * Returns all parameters for an operation, merging path-level and operation-level entries.
1195
1339
  * Operation-level parameters override path-level ones with the same `in:name` key.
1196
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
1340
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
1197
1341
  *
1198
1342
  * @example
1199
1343
  * ```ts
@@ -1294,7 +1438,10 @@ function getRequestSchema(document, operation, options = {}) {
1294
1438
  * ```ts
1295
1439
  * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
1296
1440
  * // { type: 'object', properties: {}, description: 'A pet' }
1441
+ * ```
1297
1442
  *
1443
+ * @example
1444
+ * ```ts
1298
1445
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
1299
1446
  * // returned unchanged, contains a $ref
1300
1447
  * ```
@@ -1988,7 +2135,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1988
2135
  nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
1989
2136
  },
1990
2137
  required
1991
- }, dialect);
2138
+ });
1992
2139
  }) : [];
1993
2140
  const additionalProperties = schema.additionalProperties;
1994
2141
  const additionalPropertiesNode = (() => {
@@ -2302,7 +2449,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2302
2449
  description: param["description"] ?? schema.description
2303
2450
  },
2304
2451
  required
2305
- }, dialect);
2452
+ });
2306
2453
  }
2307
2454
  /**
2308
2455
  * Reads the inline `requestBody` metadata (description / required) that OAS exposes
@@ -2355,7 +2502,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2355
2502
  if (!schema) return [];
2356
2503
  return [ast.factory.createContent({
2357
2504
  contentType: ct,
2358
- schema: dialect.schema.optionality(parseSchema({
2505
+ schema: ast.optionality(parseSchema({
2359
2506
  schema,
2360
2507
  name: requestBodyName
2361
2508
  }, options), requestBodyMeta.required),
@@ -2558,30 +2705,15 @@ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions
2558
2705
  }
2559
2706
  const circularSchemas = new Set(circularNames);
2560
2707
  const usedNames = new Set(Object.keys(schemas));
2561
- dedupePlan = ast.buildDedupePlan([...allNodes, ...operationNodes], {
2562
- isCandidate: (node) => {
2563
- if (node.type === "enum") return true;
2564
- if (node.type !== "object") return false;
2565
- if (node.name && circularSchemas.has(node.name)) return false;
2566
- return !containsCircularRef(node, { circularSchemas });
2567
- },
2568
- nameFor: (node) => {
2569
- const base = node.name;
2570
- if (!base) return null;
2571
- let name = base;
2572
- let counter = 2;
2573
- while (usedNames.has(name)) name = `${base}${counter++}`;
2574
- usedNames.add(name);
2575
- return name;
2576
- },
2577
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`
2708
+ dedupePlan = oasDialect.dedupe.plan([...allNodes, ...operationNodes], {
2709
+ circularSchemas,
2710
+ usedNames
2578
2711
  });
2579
- for (const definition of dedupePlan.hoisted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2712
+ for (const definition of dedupePlan.extracted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2580
2713
  }
2581
- const aliasNames = dedupePlan?.aliasNames;
2582
2714
  return {
2583
2715
  refAliasMap,
2584
- enumNames: aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames,
2716
+ enumNames: dedupePlan ? enumNames.filter((name) => !dedupePlan.isAlias(name)) : enumNames,
2585
2717
  circularNames,
2586
2718
  discriminatorChildMap,
2587
2719
  dedupePlan
@@ -2606,39 +2738,29 @@ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions
2606
2738
  * ```
2607
2739
  */
2608
2740
  function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2609
- const rewriteTopLevelSchema = (node) => {
2610
- if (!dedupePlan) return node;
2611
- const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node));
2612
- if (canonical && canonical.name !== node.name) return ast.factory.createSchema({
2613
- type: "ref",
2614
- name: node.name ?? null,
2615
- ref: canonical.ref,
2616
- description: node.description,
2617
- deprecated: node.deprecated
2618
- });
2619
- return ast.applyDedupe(node, dedupePlan, true);
2620
- };
2621
2741
  const schemasIterable = { [Symbol.asyncIterator]() {
2622
2742
  return (async function* () {
2623
- if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2743
+ if (dedupePlan) for (const definition of dedupePlan.extracted) yield definition;
2624
2744
  for (const [name, schema] of Object.entries(schemas)) {
2625
- if (dedupePlan?.aliasNames.has(name)) continue;
2745
+ if (dedupePlan?.isAlias(name)) continue;
2626
2746
  const alias = refAliasMap.get(name);
2627
2747
  if (alias?.name && schemas[alias.name]) {
2628
- yield rewriteTopLevelSchema({
2748
+ const aliasNode = {
2629
2749
  ...parseSchema({
2630
2750
  schema: schemas[alias.name],
2631
2751
  name: alias.name
2632
2752
  }, parserOptions),
2633
2753
  name
2634
- });
2754
+ };
2755
+ yield dedupePlan ? dedupePlan.applyTopLevel(aliasNode) : aliasNode;
2635
2756
  continue;
2636
2757
  }
2637
2758
  const parsed = parseSchema({
2638
2759
  schema,
2639
2760
  name
2640
2761
  }, parserOptions);
2641
- yield rewriteTopLevelSchema(discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed);
2762
+ const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed;
2763
+ yield dedupePlan ? dedupePlan.applyTopLevel(node) : node;
2642
2764
  }
2643
2765
  })();
2644
2766
  } };
@@ -2646,7 +2768,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, document, par
2646
2768
  return (async function* () {
2647
2769
  for (const operation of getOperations(document)) {
2648
2770
  const node = parseOperation(parserOptions, operation);
2649
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node;
2771
+ if (node) yield dedupePlan ? dedupePlan.apply(node) : node;
2650
2772
  }
2651
2773
  })();
2652
2774
  } };
@@ -2660,7 +2782,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, document, par
2660
2782
  //#endregion
2661
2783
  //#region src/adapter.ts
2662
2784
  /**
2663
- * Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
2785
+ * The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
2664
2786
  */
2665
2787
  const adapterOasName = "oas";
2666
2788
  /**