@kubb/adapter-oas 5.0.0-beta.30 → 5.0.0-beta.31

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
@@ -476,6 +476,18 @@ type AdapterOasOptions = {
476
476
  * @default 'strict'
477
477
  */
478
478
  discriminator?: 'strict' | 'inherit';
479
+ /**
480
+ * Collapse structurally identical schemas and enums into a single shared definition.
481
+ *
482
+ * Duplicated inline shapes (especially enums repeated across many properties) are hoisted
483
+ * into one named schema; every other occurrence — and any structurally identical top-level
484
+ * component — becomes a `ref` to it. Equality is shape-only: documentation such as
485
+ * `description` and `example` is ignored. Enabled by default; set to `false` to keep every
486
+ * occurrence inline and produce byte-for-byte identical output to earlier versions.
487
+ *
488
+ * @default true
489
+ */
490
+ dedupe?: boolean;
479
491
  } & Partial<ast.ParserOptions>;
480
492
  /**
481
493
  * Adapter options after defaults have been applied and schema name collisions resolved.
@@ -486,6 +498,7 @@ type AdapterOasResolvedOptions = {
486
498
  serverIndex: AdapterOasOptions['serverIndex'];
487
499
  serverVariables: AdapterOasOptions['serverVariables'];
488
500
  discriminator: NonNullable<AdapterOasOptions['discriminator']>;
501
+ dedupe: NonNullable<AdapterOasOptions['dedupe']>;
489
502
  dateType: NonNullable<AdapterOasOptions['dateType']>;
490
503
  integerType: NonNullable<AdapterOasOptions['integerType']>;
491
504
  unknownType: NonNullable<AdapterOasOptions['unknownType']>;
package/dist/index.js CHANGED
@@ -1484,6 +1484,15 @@ function createSchemaParser(ctx, dialect = oasDialect) {
1484
1484
  }, rawOptions);
1485
1485
  const nullInEnum = schema.enum.includes(null);
1486
1486
  const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
1487
+ if (nullInEnum && filteredValues.length === 0) return ast.createSchema({
1488
+ type: "null",
1489
+ primitive: "null",
1490
+ name,
1491
+ title: schema.title,
1492
+ description: schema.description,
1493
+ deprecated: schema.deprecated,
1494
+ format: schema.format
1495
+ });
1487
1496
  const enumNullable = nullable || nullInEnum || void 0;
1488
1497
  const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
1489
1498
  const enumPrimitive = getPrimitiveType(type);
@@ -2065,6 +2074,36 @@ function patchDiscriminatorNode(node, entry) {
2065
2074
  //#endregion
2066
2075
  //#region src/stream.ts
2067
2076
  /**
2077
+ * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
2078
+ * single extra parse pass over operations (so duplicates in request/response bodies are seen).
2079
+ *
2080
+ * Only enums and objects are candidates, and object shapes that reference a circular schema are
2081
+ * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
2082
+ * name (collision-resolved against existing component names); shapes without a name stay inline.
2083
+ */
2084
+ function createDedupePlan({ schemaNodes, operationNodes, schemaNames, circularNames }) {
2085
+ const circularSchemas = new Set(circularNames);
2086
+ const usedNames = new Set(schemaNames);
2087
+ return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
2088
+ isCandidate: (node) => {
2089
+ if (node.type === "enum") return true;
2090
+ if (node.type !== "object") return false;
2091
+ if (node.name && circularSchemas.has(node.name)) return false;
2092
+ return !ast.containsCircularRef(node, { circularSchemas });
2093
+ },
2094
+ nameFor: (node) => {
2095
+ const base = node.name;
2096
+ if (!base) return null;
2097
+ let name = base;
2098
+ let counter = 2;
2099
+ while (usedNames.has(name)) name = `${base}${counter++}`;
2100
+ usedNames.add(name);
2101
+ return name;
2102
+ },
2103
+ refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`
2104
+ });
2105
+ }
2106
+ /**
2068
2107
  * Reads the server URL from the document's `servers` array at `serverIndex`,
2069
2108
  * interpolating any `serverVariables` into the URL template.
2070
2109
  *
@@ -2105,7 +2144,7 @@ function resolveBaseUrl({ document, serverIndex, serverVariables }) {
2105
2144
  * })
2106
2145
  * ```
2107
2146
  */
2108
- function preScan({ schemas, parseSchema, parserOptions, discriminator }) {
2147
+ function preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }) {
2109
2148
  const allNodes = [];
2110
2149
  const refAliasMap = /* @__PURE__ */ new Map();
2111
2150
  const enumNames = [];
@@ -2120,11 +2159,30 @@ function preScan({ schemas, parseSchema, parserOptions, discriminator }) {
2120
2159
  if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) enumNames.push(node.name);
2121
2160
  if (discriminator === "inherit" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2122
2161
  }
2162
+ const circularNames = [...ast.findCircularSchemas(allNodes)];
2163
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
2164
+ let dedupePlan = null;
2165
+ if (dedupe) {
2166
+ const operationNodes = [];
2167
+ for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2168
+ if (!operation) continue;
2169
+ const operationNode = parseOperation(parserOptions, operation);
2170
+ if (operationNode) operationNodes.push(operationNode);
2171
+ }
2172
+ dedupePlan = createDedupePlan({
2173
+ schemaNodes: allNodes,
2174
+ operationNodes,
2175
+ schemaNames: Object.keys(schemas),
2176
+ circularNames
2177
+ });
2178
+ for (const definition of dedupePlan.hoisted) if (definition.type === "enum" && definition.name) enumNames.push(definition.name);
2179
+ }
2123
2180
  return {
2124
2181
  refAliasMap,
2125
2182
  enumNames,
2126
- circularNames: [...ast.findCircularSchemas(allNodes)],
2127
- discriminatorChildMap: discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
2183
+ circularNames,
2184
+ discriminatorChildMap,
2185
+ dedupePlan
2128
2186
  };
2129
2187
  }
2130
2188
  /**
@@ -2145,26 +2203,39 @@ function preScan({ schemas, parseSchema, parserOptions, discriminator }) {
2145
2203
  * }
2146
2204
  * ```
2147
2205
  */
2148
- function createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta }) {
2206
+ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, dedupePlan, meta }) {
2207
+ const rewriteTopLevelSchema = (node) => {
2208
+ if (!dedupePlan) return node;
2209
+ const canonical = dedupePlan.canonicalBySignature.get(ast.schemaSignature(node));
2210
+ if (canonical && canonical.name !== node.name) return ast.createSchema({
2211
+ type: "ref",
2212
+ name: node.name ?? null,
2213
+ ref: canonical.ref,
2214
+ description: node.description,
2215
+ deprecated: node.deprecated
2216
+ });
2217
+ return ast.applyDedupe(node, dedupePlan.canonicalBySignature, true);
2218
+ };
2149
2219
  const schemasIterable = { [Symbol.asyncIterator]() {
2150
2220
  return (async function* () {
2221
+ if (dedupePlan) for (const definition of dedupePlan.hoisted) yield definition;
2151
2222
  for (const [name, schema] of Object.entries(schemas)) {
2152
2223
  const alias = refAliasMap.get(name);
2153
2224
  if (alias?.name && schemas[alias.name]) {
2154
- yield {
2225
+ yield rewriteTopLevelSchema({
2155
2226
  ...parseSchema({
2156
2227
  schema: schemas[alias.name],
2157
2228
  name: alias.name
2158
2229
  }, parserOptions),
2159
2230
  name
2160
- };
2231
+ });
2161
2232
  continue;
2162
2233
  }
2163
2234
  const parsed = parseSchema({
2164
2235
  schema,
2165
2236
  name
2166
2237
  }, parserOptions);
2167
- yield discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed;
2238
+ yield rewriteTopLevelSchema(discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)) : parsed);
2168
2239
  }
2169
2240
  })();
2170
2241
  } };
@@ -2173,7 +2244,7 @@ function createInputStream({ schemas, parseSchema, parseOperation, baseOas, pars
2173
2244
  for (const methods of Object.values(baseOas.getPaths())) for (const operation of Object.values(methods)) {
2174
2245
  if (!operation) continue;
2175
2246
  const node = parseOperation(parserOptions, operation);
2176
- if (node) yield node;
2247
+ if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan.canonicalBySignature) : node;
2177
2248
  }
2178
2249
  })();
2179
2250
  } };
@@ -2213,7 +2284,7 @@ const adapterOasName = "oas";
2213
2284
  * ```
2214
2285
  */
2215
2286
  const adapterOas = createAdapter((options) => {
2216
- const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
2287
+ const { validate = true, contentType, serverIndex, serverVariables, discriminator = "strict", dedupe = true, dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
2217
2288
  const parserOptions = {
2218
2289
  ...DEFAULT_PARSER_OPTIONS,
2219
2290
  dateType,
@@ -2240,25 +2311,30 @@ const adapterOas = createAdapter((options) => {
2240
2311
  document,
2241
2312
  contentType
2242
2313
  }));
2243
- const ensurePreScan = once((schemas, parseSchema) => preScan({
2314
+ const ensurePreScan = once((schemas, parseSchema, parseOperation, baseOas) => preScan({
2244
2315
  schemas,
2245
2316
  parseSchema,
2317
+ parseOperation,
2318
+ baseOas,
2246
2319
  parserOptions,
2247
- discriminator
2320
+ discriminator,
2321
+ dedupe
2248
2322
  }));
2249
2323
  async function createStream(source) {
2250
2324
  const document = await ensureDocument(source);
2251
2325
  const schemas = await ensureSchemas(document);
2252
2326
  const { parseSchema, parseOperation } = ensureSchemaParser(document);
2253
- const { refAliasMap, enumNames, circularNames, discriminatorChildMap } = ensurePreScan(schemas, parseSchema);
2327
+ const baseOas = ensureBaseOas(document);
2328
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas);
2254
2329
  return createInputStream({
2255
2330
  schemas,
2256
2331
  parseSchema,
2257
2332
  parseOperation,
2258
- baseOas: ensureBaseOas(document),
2333
+ baseOas,
2259
2334
  parserOptions,
2260
2335
  refAliasMap,
2261
2336
  discriminatorChildMap,
2337
+ dedupePlan,
2262
2338
  meta: {
2263
2339
  title: document.info?.title,
2264
2340
  description: document.info?.description,
@@ -2282,6 +2358,7 @@ const adapterOas = createAdapter((options) => {
2282
2358
  serverIndex,
2283
2359
  serverVariables,
2284
2360
  discriminator,
2361
+ dedupe,
2285
2362
  dateType,
2286
2363
  integerType,
2287
2364
  unknownType,