@kubb/adapter-oas 5.0.0-beta.61 → 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.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) {
@@ -832,18 +994,21 @@ function dereferenceWithRef(document, schema) {
832
994
  * const parser = createSchemaParser(context, oasDialect) // explicit
833
995
  * ```
834
996
  */
835
- const oasDialect = ast.defineSchemaDialect({
997
+ const oasDialect = ast.defineDialect({
836
998
  name: "oas",
837
- isNullable,
838
- isReference,
839
- isDiscriminator,
840
- isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
841
- resolveRef
999
+ schema: {
1000
+ isNullable,
1001
+ isReference,
1002
+ isDiscriminator,
1003
+ isBinary: (schema) => schema.type === "string" && schema.contentMediaType === "application/octet-stream",
1004
+ resolveRef
1005
+ },
1006
+ dedupe: { plan }
842
1007
  });
843
1008
  //#endregion
844
1009
  //#region src/discriminator.ts
845
1010
  /**
846
- * 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
847
1012
  * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
848
1013
  *
849
1014
  * The streaming path calls this on a small pre-parsed subset of schemas (only the
@@ -959,9 +1124,8 @@ function findDiscriminator(mapping, ref) {
959
1124
  /**
960
1125
  * MIME type fragments that mark a media type as JSON-like.
961
1126
  *
962
- * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
963
- * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
964
- * `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`.
965
1129
  */
966
1130
  const jsonMimeFragments = [
967
1131
  "application/json",
@@ -1057,8 +1221,8 @@ function getRequestContent({ document, operation, mediaType }) {
1057
1221
  return available ? [available, content[available]] : false;
1058
1222
  }
1059
1223
  /**
1060
- * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
1061
- * 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'`.
1062
1226
  */
1063
1227
  function getRequestContentType({ document, operation }) {
1064
1228
  const content = getRequestBodyContent({
@@ -1153,9 +1317,9 @@ function getSchemaType(format) {
1153
1317
  return formatMap[format] ?? null;
1154
1318
  }
1155
1319
  /**
1156
- * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
1157
- * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
1158
- * 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
1159
1323
  * diagnostic in step with the parser as `formatMap` grows.
1160
1324
  */
1161
1325
  function isHandledFormat(format) {
@@ -1173,7 +1337,7 @@ function getPrimitiveType(type) {
1173
1337
  /**
1174
1338
  * Returns all parameters for an operation, merging path-level and operation-level entries.
1175
1339
  * Operation-level parameters override path-level ones with the same `in:name` key.
1176
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
1340
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
1177
1341
  *
1178
1342
  * @example
1179
1343
  * ```ts
@@ -1274,7 +1438,10 @@ function getRequestSchema(document, operation, options = {}) {
1274
1438
  * ```ts
1275
1439
  * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
1276
1440
  * // { type: 'object', properties: {}, description: 'A pet' }
1441
+ * ```
1277
1442
  *
1443
+ * @example
1444
+ * ```ts
1278
1445
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
1279
1446
  * // returned unchanged, contains a $ref
1280
1447
  * ```
@@ -1561,6 +1728,21 @@ function normalizeArrayEnum(schema) {
1561
1728
  };
1562
1729
  }
1563
1730
  /**
1731
+ * Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
1732
+ * and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
1733
+ */
1734
+ function createNullSchema(schema, name) {
1735
+ return ast.factory.createSchema({
1736
+ type: "null",
1737
+ primitive: "null",
1738
+ name,
1739
+ title: schema.title,
1740
+ description: schema.description,
1741
+ deprecated: schema.deprecated,
1742
+ format: schema.format
1743
+ });
1744
+ }
1745
+ /**
1564
1746
  * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
1565
1747
  * the parent and property name. Wraps `macroEnumName` at the property construction site.
1566
1748
  */
@@ -1616,7 +1798,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1616
1798
  if (refPath && !resolvingRefs.has(refPath)) {
1617
1799
  if (!resolvedRefCache.has(refPath)) {
1618
1800
  try {
1619
- const referenced = dialect.resolveRef(document, refPath);
1801
+ const referenced = dialect.schema.resolveRef(document, refPath);
1620
1802
  if (referenced) {
1621
1803
  resolvingRefs.add(refPath);
1622
1804
  resolvedSchema = parseSchema({ schema: referenced }, rawOptions);
@@ -1665,13 +1847,13 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1665
1847
  }
1666
1848
  const filteredDiscriminantValues = [];
1667
1849
  const allOfMembers = schema.allOf.filter((item) => {
1668
- if (!dialect.isReference(item) || !name) return true;
1669
- const deref = dialect.resolveRef(document, item.$ref);
1670
- if (!deref || !dialect.isDiscriminator(deref)) return true;
1850
+ if (!dialect.schema.isReference(item) || !name) return true;
1851
+ const deref = dialect.schema.resolveRef(document, item.$ref);
1852
+ if (!deref || !dialect.schema.isDiscriminator(deref)) return true;
1671
1853
  const parentUnion = deref.oneOf ?? deref.anyOf;
1672
1854
  if (!parentUnion) return true;
1673
1855
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1674
- const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef);
1856
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.schema.isReference(oneOfItem) && oneOfItem.$ref === childRef);
1675
1857
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1676
1858
  if (inOneOf || inMapping) {
1677
1859
  const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
@@ -1692,9 +1874,9 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1692
1874
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
1693
1875
  if (missingRequired.length) {
1694
1876
  const resolvedMembers = schema.allOf.flatMap((item) => {
1695
- if (!dialect.isReference(item)) return [item];
1696
- const deref = dialect.resolveRef(document, item.$ref);
1697
- return deref && !dialect.isReference(deref) ? [deref] : [];
1877
+ if (!dialect.schema.isReference(item)) return [item];
1878
+ const deref = dialect.schema.resolveRef(document, item.$ref);
1879
+ return deref && !dialect.schema.isReference(deref) ? [deref] : [];
1698
1880
  });
1699
1881
  for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
1700
1882
  allOfMembers.push(parseSchema({
@@ -1739,10 +1921,10 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1739
1921
  const strategy = schema.oneOf ? "one" : "any";
1740
1922
  const unionBase = {
1741
1923
  ...buildSchemaNode(schema, name, nullable, defaultValue),
1742
- discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1924
+ discriminatorPropertyName: dialect.schema.isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
1743
1925
  strategy
1744
1926
  };
1745
- const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : void 0;
1927
+ const discriminator = dialect.schema.isDiscriminator(schema) ? schema.discriminator : void 0;
1746
1928
  const sharedPropertiesNode = schema.properties ? (() => {
1747
1929
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema;
1748
1930
  return parseSchema({
@@ -1752,7 +1934,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1752
1934
  })() : void 0;
1753
1935
  if (sharedPropertiesNode || discriminator?.mapping) {
1754
1936
  const members = unionMembers.map((s) => {
1755
- const ref = dialect.isReference(s) ? s.$ref : void 0;
1937
+ const ref = dialect.schema.isReference(s) ? s.$ref : void 0;
1756
1938
  const discriminatorValue = findDiscriminator(discriminator?.mapping, ref);
1757
1939
  const memberNode = parseSchema({
1758
1940
  schema: s,
@@ -1798,15 +1980,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1798
1980
  */
1799
1981
  function convertConst({ schema, name, nullable, defaultValue }) {
1800
1982
  const constValue = schema.const;
1801
- if (constValue === null) return ast.factory.createSchema({
1802
- type: "null",
1803
- primitive: "null",
1804
- name,
1805
- title: schema.title,
1806
- description: schema.description,
1807
- deprecated: schema.deprecated,
1808
- format: schema.format
1809
- });
1983
+ if (constValue === null) return createNullSchema(schema, name);
1810
1984
  const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
1811
1985
  return ast.factory.createSchema({
1812
1986
  type: "enum",
@@ -1895,15 +2069,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1895
2069
  }, rawOptions);
1896
2070
  const nullInEnum = schema.enum.includes(null);
1897
2071
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1898
- if (nullInEnum && filteredValues.length === 0) return ast.factory.createSchema({
1899
- type: "null",
1900
- primitive: "null",
1901
- name,
1902
- title: schema.title,
1903
- description: schema.description,
1904
- deprecated: schema.deprecated,
1905
- format: schema.format
1906
- });
2072
+ if (nullInEnum && filteredValues.length === 0) return createNullSchema(schema, name);
1907
2073
  const enumNullable = nullable || nullInEnum || void 0;
1908
2074
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1909
2075
  const enumPrimitive = getPrimitiveType(type);
@@ -1953,7 +2119,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1953
2119
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
1954
2120
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
1955
2121
  const resolvedPropSchema = propSchema;
1956
- const propNullable = dialect.isNullable(resolvedPropSchema);
2122
+ const propNullable = dialect.schema.isNullable(resolvedPropSchema);
1957
2123
  const schemaNode = nameEnums(parseSchema({
1958
2124
  schema: resolvedPropSchema,
1959
2125
  name: childName(name, propName)
@@ -1990,7 +2156,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1990
2156
  maxProperties: schema.maxProperties,
1991
2157
  ...buildSchemaNode(schema, name, nullable, defaultValue)
1992
2158
  });
1993
- if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
2159
+ if (dialect.schema.isDiscriminator(schema) && schema.discriminator.mapping) {
1994
2160
  const discPropName = schema.discriminator.propertyName;
1995
2161
  const values = Object.keys(schema.discriminator.mapping);
1996
2162
  const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
@@ -2134,7 +2300,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2134
2300
  const schemaRules = [
2135
2301
  {
2136
2302
  name: "ref",
2137
- match: ({ schema }) => dialect.isReference(schema),
2303
+ match: ({ schema }) => dialect.schema.isReference(schema),
2138
2304
  convert: convertRef
2139
2305
  },
2140
2306
  {
@@ -2159,7 +2325,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2159
2325
  },
2160
2326
  {
2161
2327
  name: "blob",
2162
- match: ({ schema }) => dialect.isBinary(schema),
2328
+ match: ({ schema }) => dialect.schema.isBinary(schema),
2163
2329
  convert: convertBlob
2164
2330
  },
2165
2331
  {
@@ -2240,7 +2406,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2240
2406
  schema: flattenedSchema,
2241
2407
  name
2242
2408
  }, rawOptions);
2243
- const nullable = dialect.isNullable(schema) || void 0;
2409
+ const nullable = dialect.schema.isNullable(schema) || void 0;
2244
2410
  const schemaCtx = {
2245
2411
  schema,
2246
2412
  name,
@@ -2317,7 +2483,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2317
2483
  const keys = [];
2318
2484
  for (const key in schema.properties) {
2319
2485
  const prop = schema.properties[key];
2320
- if (prop && !dialect.isReference(prop) && prop[flag]) keys.push(key);
2486
+ if (prop && !dialect.schema.isReference(prop) && prop[flag]) keys.push(key);
2321
2487
  }
2322
2488
  return keys.length ? keys : null;
2323
2489
  }
@@ -2334,14 +2500,14 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2334
2500
  const content = allContentTypes.flatMap((ct) => {
2335
2501
  const schema = getRequestSchema(document, operation, { contentType: ct });
2336
2502
  if (!schema) return [];
2337
- return [{
2503
+ return [ast.factory.createContent({
2338
2504
  contentType: ct,
2339
- schema: ast.syncOptionality(parseSchema({
2505
+ schema: ast.optionality(parseSchema({
2340
2506
  schema,
2341
2507
  name: requestBodyName
2342
2508
  }, options), requestBodyMeta.required),
2343
2509
  keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
2344
- }];
2510
+ })];
2345
2511
  });
2346
2512
  const requestBody = content.length > 0 || requestBodyMeta.description ? {
2347
2513
  description: requestBodyMeta.description,
@@ -2366,17 +2532,17 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2366
2532
  keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
2367
2533
  };
2368
2534
  };
2369
- const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ({
2535
+ const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ast.factory.createContent({
2370
2536
  contentType,
2371
2537
  ...parseEntrySchema(contentType)
2372
2538
  }));
2373
- if (content.length === 0) content.push({
2539
+ if (content.length === 0) content.push(ast.factory.createContent({
2374
2540
  contentType: getRequestContentType({
2375
2541
  document,
2376
2542
  operation
2377
2543
  }) || "application/json",
2378
2544
  ...parseEntrySchema(ctx.contentType)
2379
- });
2545
+ }));
2380
2546
  return ast.factory.createResponse({
2381
2547
  statusCode,
2382
2548
  description,
@@ -2384,7 +2550,7 @@ function createSchemaParser(ctx, dialect = oasDialect) {
2384
2550
  });
2385
2551
  });
2386
2552
  const pathItem = document.paths?.[operation.path];
2387
- const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? pathItem : void 0;
2553
+ const pathItemDoc = pathItem && !dialect.schema.isReference(pathItem) ? pathItem : void 0;
2388
2554
  const pickDoc = (key) => {
2389
2555
  const own = operation.schema[key];
2390
2556
  if (typeof own === "string") return own;
@@ -2469,36 +2635,6 @@ function visit(node, pointer) {
2469
2635
  //#endregion
2470
2636
  //#region src/stream.ts
2471
2637
  /**
2472
- * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
2473
- * single extra parse pass over operations (so duplicates in request/response bodies are seen).
2474
- *
2475
- * Only enums and objects are candidates, and object shapes that reference a circular schema are
2476
- * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
2477
- * name (collision-resolved against existing component names). Shapes without a name stay inline.
2478
- */
2479
- function createDedupePlan({ schemaNodes, operationNodes, schemaNames, circularNames }) {
2480
- const circularSchemas = new Set(circularNames);
2481
- const usedNames = new Set(schemaNames);
2482
- return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
2483
- isCandidate: (node) => {
2484
- if (node.type === "enum") return true;
2485
- if (node.type !== "object") return false;
2486
- if (node.name && circularSchemas.has(node.name)) return false;
2487
- return !containsCircularRef(node, { circularSchemas });
2488
- },
2489
- nameFor: (node) => {
2490
- const base = node.name;
2491
- if (!base) return null;
2492
- let name = base;
2493
- let counter = 2;
2494
- while (usedNames.has(name)) name = `${base}${counter++}`;
2495
- usedNames.add(name);
2496
- return name;
2497
- },
2498
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`
2499
- });
2500
- }
2501
- /**
2502
2638
  * Reads the server URL from the document's `servers` array at `serverIndex`,
2503
2639
  * interpolating any `serverVariables` into the URL template.
2504
2640
  *
@@ -2567,18 +2703,17 @@ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions
2567
2703
  const operationNode = parseOperation(parserOptions, operation);
2568
2704
  if (operationNode) operationNodes.push(operationNode);
2569
2705
  }
2570
- dedupePlan = createDedupePlan({
2571
- schemaNodes: allNodes,
2572
- operationNodes,
2573
- schemaNames: Object.keys(schemas),
2574
- circularNames
2706
+ const circularSchemas = new Set(circularNames);
2707
+ const usedNames = new Set(Object.keys(schemas));
2708
+ dedupePlan = oasDialect.dedupe.plan([...allNodes, ...operationNodes], {
2709
+ circularSchemas,
2710
+ usedNames
2575
2711
  });
2576
- 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);
2577
2713
  }
2578
- const aliasNames = dedupePlan?.aliasNames;
2579
2714
  return {
2580
2715
  refAliasMap,
2581
- enumNames: aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames,
2716
+ enumNames: dedupePlan ? enumNames.filter((name) => !dedupePlan.isAlias(name)) : enumNames,
2582
2717
  circularNames,
2583
2718
  discriminatorChildMap,
2584
2719
  dedupePlan
@@ -2603,39 +2738,29 @@ function preScan({ schemas, parseSchema, parseOperation, document, parserOptions
2603
2738
  * ```
2604
2739
  */
2605
2740
  function createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2606
- const rewriteTopLevelSchema = (node) => {
2607
- if (!dedupePlan) return node;
2608
- const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node));
2609
- if (canonical && canonical.name !== node.name) return ast.factory.createSchema({
2610
- type: "ref",
2611
- name: node.name ?? null,
2612
- ref: canonical.ref,
2613
- description: node.description,
2614
- deprecated: node.deprecated
2615
- });
2616
- return ast.applyDedupe(node, dedupePlan, true);
2617
- };
2618
2741
  const schemasIterable = { [Symbol.asyncIterator]() {
2619
2742
  return (async function* () {
2620
- if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2743
+ if (dedupePlan) for (const definition of dedupePlan.extracted) yield definition;
2621
2744
  for (const [name, schema] of Object.entries(schemas)) {
2622
- if (dedupePlan?.aliasNames.has(name)) continue;
2745
+ if (dedupePlan?.isAlias(name)) continue;
2623
2746
  const alias = refAliasMap.get(name);
2624
2747
  if (alias?.name && schemas[alias.name]) {
2625
- yield rewriteTopLevelSchema({
2748
+ const aliasNode = {
2626
2749
  ...parseSchema({
2627
2750
  schema: schemas[alias.name],
2628
2751
  name: alias.name
2629
2752
  }, parserOptions),
2630
2753
  name
2631
- });
2754
+ };
2755
+ yield dedupePlan ? dedupePlan.applyTopLevel(aliasNode) : aliasNode;
2632
2756
  continue;
2633
2757
  }
2634
2758
  const parsed = parseSchema({
2635
2759
  schema,
2636
2760
  name
2637
2761
  }, parserOptions);
2638
- 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;
2639
2764
  }
2640
2765
  })();
2641
2766
  } };
@@ -2643,7 +2768,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, document, par
2643
2768
  return (async function* () {
2644
2769
  for (const operation of getOperations(document)) {
2645
2770
  const node = parseOperation(parserOptions, operation);
2646
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node;
2771
+ if (node) yield dedupePlan ? dedupePlan.apply(node) : node;
2647
2772
  }
2648
2773
  })();
2649
2774
  } };
@@ -2657,7 +2782,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, document, par
2657
2782
  //#endregion
2658
2783
  //#region src/adapter.ts
2659
2784
  /**
2660
- * 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.
2661
2786
  */
2662
2787
  const adapterOasName = "oas";
2663
2788
  /**