@kubb/adapter-oas 5.0.0-beta.3 → 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/src/refs.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { isReference } from './guards.ts'
2
2
  import type { Document } from './types.ts'
3
3
 
4
+ const _refCache = new WeakMap<Document, Map<string, unknown>>()
5
+
4
6
  /**
5
7
  * Resolves a local JSON pointer reference from a document.
6
8
  *
@@ -18,11 +20,19 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
18
20
  if ($ref === '') {
19
21
  return null
20
22
  }
21
- if ($ref.startsWith('#')) {
22
- $ref = globalThis.decodeURIComponent($ref.substring(1))
23
- } else {
24
- return null
23
+ if (!$ref.startsWith('#')) return null
24
+ $ref = globalThis.decodeURIComponent($ref.substring(1))
25
+
26
+ let docCache = _refCache.get(document)
27
+ if (!docCache) {
28
+ docCache = new Map()
29
+ _refCache.set(document, docCache)
25
30
  }
31
+
32
+ if (docCache.has($ref)) {
33
+ return docCache.get($ref) as T
34
+ }
35
+
26
36
  const current = $ref
27
37
  .split('/')
28
38
  .filter(Boolean)
@@ -31,6 +41,8 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
31
41
  if (!current) {
32
42
  throw new Error(`Could not find a definition for ${origRef}.`)
33
43
  }
44
+
45
+ docCache.set($ref, current)
34
46
  return current as T
35
47
  }
36
48
 
package/src/resolvers.ts CHANGED
@@ -86,7 +86,7 @@ export type OperationsOptions = {
86
86
  * ```
87
87
  */
88
88
  export function getParameters(document: Document, operation: Operation): Array<ParameterObject> {
89
- const resolveParams = (params: unknown[]): Array<ParameterObject> =>
89
+ const resolveParams = (params: Array<unknown>): Array<ParameterObject> =>
90
90
  params.map((p) => dereferenceWithRef(document, p)).filter((p): p is ParameterObject => !!p && typeof p === 'object' && 'in' in p && 'name' in p)
91
91
 
92
92
  const operationParams = resolveParams(operation.schema?.parameters || [])
@@ -108,7 +108,7 @@ export function getParameters(document: Document, operation: Operation): Array<P
108
108
  return Array.from(paramMap.values())
109
109
  }
110
110
 
111
- function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...string[]] {
111
+ function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...Array<string>] {
112
112
  if (!responseBody) return false
113
113
  if (isReference(responseBody)) return false
114
114
 
@@ -260,7 +260,7 @@ function hasStructuralKeywords(fragment: SchemaObject): boolean {
260
260
  export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
261
261
  if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
262
262
 
263
- const allOfFragments = schema.allOf as SchemaObject[]
263
+ const allOfFragments = schema.allOf as Array<SchemaObject>
264
264
  if (allOfFragments.some((item) => isRef(item))) return schema
265
265
  if (allOfFragments.some(hasStructuralKeywords)) return schema
266
266
 
@@ -305,27 +305,25 @@ export function extractSchemaFromContent(content: Record<string, unknown> | unde
305
305
  /**
306
306
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
307
307
  */
308
- function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
308
+ function* collectRefs(schema: unknown): Generator<string, void, undefined> {
309
309
  if (Array.isArray(schema)) {
310
- for (const item of schema) collectRefs(item, refs)
311
- return refs
310
+ for (const item of schema) yield* collectRefs(item)
311
+ return
312
312
  }
313
313
 
314
314
  if (schema && typeof schema === 'object') {
315
315
  for (const key in schema) {
316
316
  const value = (schema as Record<string, unknown>)[key]
317
- if (key === '$ref' && typeof value === 'string') {
318
- if (value.startsWith(SCHEMA_REF_PREFIX)) {
319
- const name = value.slice(SCHEMA_REF_PREFIX.length)
320
- if (name) refs.add(name)
321
- }
322
- } else {
323
- collectRefs(value, refs)
317
+ if (!(key === '$ref' && typeof value === 'string')) {
318
+ yield* collectRefs(value)
319
+ continue
320
+ }
321
+ if (value.startsWith(SCHEMA_REF_PREFIX)) {
322
+ const name = value.slice(SCHEMA_REF_PREFIX.length)
323
+ if (name) yield name
324
324
  }
325
325
  }
326
326
  }
327
-
328
- return refs
329
327
  }
330
328
 
331
329
  /**
@@ -341,13 +339,13 @@ function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
341
339
  * ```
342
340
  */
343
341
  export function sortSchemas(schemas: Record<string, SchemaObject>): Record<string, SchemaObject> {
344
- const deps = new Map<string, string[]>()
342
+ const deps = new Map<string, Array<string>>()
345
343
 
346
344
  for (const [name, schema] of Object.entries(schemas)) {
347
- deps.set(name, Array.from(collectRefs(schema)))
345
+ deps.set(name, [...new Set(collectRefs(schema))])
348
346
  }
349
347
 
350
- const sorted: string[] = []
348
+ const sorted: Array<string> = []
351
349
  const visited = new Set<string>()
352
350
 
353
351
  function visit(name: string, stack: Set<string>) {
@@ -404,7 +402,7 @@ function resolveSchemaRef(document: Document, schema: SchemaObject): SchemaObjec
404
402
  export function getSchemas(document: Document, { contentType }: GetSchemasOptions): GetSchemasResult {
405
403
  const components = document.components
406
404
 
407
- const candidates: SchemaWithMetadata[] = [
405
+ const candidates: Array<SchemaWithMetadata> = [
408
406
  ...Object.entries((components?.schemas as Record<string, SchemaObject>) ?? {}).map(([name, schema]) => ({
409
407
  schema: resolveSchemaRef(document, schema),
410
408
  source: 'schemas' as const,
@@ -426,7 +424,7 @@ export function getSchemas(document: Document, { contentType }: GetSchemasOption
426
424
  ),
427
425
  ]
428
426
 
429
- const normalizedNames = new Map<string, SchemaWithMetadata[]>()
427
+ const normalizedNames = new Map<string, Array<SchemaWithMetadata>>()
430
428
  for (const item of candidates) {
431
429
  const key = pascalCase(item.originalName)
432
430
  const bucket = normalizedNames.get(key) ?? []
@@ -514,6 +512,7 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
514
512
  writeOnly: schema.writeOnly,
515
513
  default: defaultValue,
516
514
  example: schema.example,
515
+ format: schema.format,
517
516
  } as const
518
517
  }
519
518
 
@@ -530,7 +529,7 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
530
529
  * // ['application/json', 'multipart/form-data']
531
530
  * ```
532
531
  */
533
- export function getRequestBodyContentTypes(document: Document, operation: Operation): string[] {
532
+ export function getRequestBodyContentTypes(document: Document, operation: Operation): Array<string> {
534
533
  if (operation.schema.requestBody) {
535
534
  operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody)
536
535
  }
@@ -542,3 +541,33 @@ export function getRequestBodyContentTypes(document: Document, operation: Operat
542
541
  // Do not bail out on isReference — the content is already present on the merged object.
543
542
  return body.content ? Object.keys(body.content) : []
544
543
  }
544
+
545
+ /**
546
+ * Returns all response content type keys for an operation at a given status code.
547
+ *
548
+ * Response `$ref`s are resolved in-place first — the same mutation `getResponseSchema` performs —
549
+ * so the returned list reflects the available content types even for referenced responses.
550
+ *
551
+ * @example
552
+ * ```ts
553
+ * getResponseBodyContentTypes(document, operation, 200)
554
+ * // ['application/json', 'application/xml']
555
+ * ```
556
+ */
557
+ export function getResponseBodyContentTypes(document: Document, operation: Operation, statusCode: string | number): Array<string> {
558
+ if (operation.schema.responses) {
559
+ const responses = operation.schema.responses
560
+ for (const key in responses) {
561
+ const schema = responses[key]
562
+ if (schema && isReference(schema)) {
563
+ responses[key] = resolveRef<any>(document, schema.$ref)
564
+ }
565
+ }
566
+ }
567
+
568
+ const responseObj = operation.getResponseByStatusCode(statusCode)
569
+ if (!responseObj || typeof responseObj !== 'object' || isReference(responseObj)) return []
570
+
571
+ const body = responseObj as { content?: Record<string, unknown> }
572
+ return body.content ? Object.keys(body.content) : []
573
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,276 @@
1
+ import { ast } from '@kubb/core'
2
+ import type BaseOas from 'oas'
3
+ import { SCHEMA_REF_PREFIX } from './constants.ts'
4
+ import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
5
+ import type { SchemaParser } from './parser.ts'
6
+ import { resolveServerUrl } from './resolvers.ts'
7
+ import type { DiscriminatorTarget } from './discriminator.ts'
8
+ import type { AdapterOas, Document, SchemaObject } from './types.ts'
9
+
10
+ export type PreScanResult = {
11
+ refAliasMap: Map<string, ast.SchemaNode>
12
+ enumNames: Array<string>
13
+ circularNames: Array<string>
14
+ discriminatorChildMap: Map<string, DiscriminatorTarget> | null
15
+ dedupePlan: ast.DedupePlan | null
16
+ }
17
+
18
+ /**
19
+ * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
20
+ * single extra parse pass over operations (so duplicates in request/response bodies are seen).
21
+ *
22
+ * Only enums and objects are candidates, and object shapes that reference a circular schema are
23
+ * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
24
+ * name (collision-resolved against existing component names); shapes without a name stay inline.
25
+ */
26
+ function createDedupePlan({
27
+ schemaNodes,
28
+ operationNodes,
29
+ schemaNames,
30
+ circularNames,
31
+ }: {
32
+ schemaNodes: Array<ast.SchemaNode>
33
+ operationNodes: Array<ast.OperationNode>
34
+ schemaNames: Array<string>
35
+ circularNames: Array<string>
36
+ }): ast.DedupePlan {
37
+ const circularSchemas = new Set(circularNames)
38
+ const usedNames = new Set(schemaNames)
39
+
40
+ return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
41
+ isCandidate: (node) => {
42
+ if (node.type === 'enum') return true
43
+ if (node.type !== 'object') return false
44
+ // Skip object shapes that are part of a circular chain — hoisting them would break the cycle.
45
+ if (node.name && circularSchemas.has(node.name)) return false
46
+ return !ast.containsCircularRef(node, { circularSchemas })
47
+ },
48
+ nameFor: (node) => {
49
+ const base = node.name
50
+ if (!base) return null
51
+
52
+ let name = base
53
+ let counter = 2
54
+ while (usedNames.has(name)) {
55
+ name = `${base}${counter++}`
56
+ }
57
+ usedNames.add(name)
58
+ return name
59
+ },
60
+ refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
61
+ })
62
+ }
63
+
64
+ /**
65
+ * Reads the server URL from the document's `servers` array at `serverIndex`,
66
+ * interpolating any `serverVariables` into the URL template.
67
+ *
68
+ * Returns `null` when `serverIndex` is omitted or out of range.
69
+ *
70
+ * @example Resolve the first server
71
+ * `resolveBaseUrl({ document, serverIndex: 0 })`
72
+ *
73
+ * @example Override a path variable
74
+ * `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
75
+ */
76
+ export function resolveBaseUrl({
77
+ document,
78
+ serverIndex,
79
+ serverVariables,
80
+ }: {
81
+ document: Document
82
+ serverIndex?: number
83
+ serverVariables?: Record<string, string>
84
+ }): string | null {
85
+ const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
86
+ return server?.url ? resolveServerUrl(server, serverVariables) : null
87
+ }
88
+
89
+ /**
90
+ * Parses every schema once to build the lookup structures that streaming needs upfront.
91
+ *
92
+ * Three things happen in this single pass:
93
+ * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
94
+ * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
95
+ * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
96
+ * The `allNodes` array is local and drops out of scope as soon as this function returns.
97
+ *
98
+ * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
99
+ * Both are proportional to the number of aliases or discriminator parents, not total schema count.
100
+ *
101
+ * Each schema is parsed again during the streaming pass — this is intentional.
102
+ * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const { refAliasMap, enumNames, circularNames } = preScan({
107
+ * schemas,
108
+ * parseSchema,
109
+ * parserOptions,
110
+ * discriminator: 'strict',
111
+ * })
112
+ * ```
113
+ */
114
+ export function preScan({
115
+ schemas,
116
+ parseSchema,
117
+ parseOperation,
118
+ baseOas,
119
+ parserOptions,
120
+ discriminator,
121
+ dedupe,
122
+ }: {
123
+ schemas: Record<string, SchemaObject>
124
+ parseSchema: (entry: { schema: SchemaObject; name: string }, options: ast.ParserOptions) => ast.SchemaNode
125
+ parseOperation: SchemaParser['parseOperation']
126
+ baseOas: BaseOas
127
+ parserOptions: ast.ParserOptions
128
+ discriminator: AdapterOas['options']['discriminator']
129
+ dedupe: boolean
130
+ }): PreScanResult {
131
+ const allNodes: Array<ast.SchemaNode> = []
132
+ const refAliasMap = new Map<string, ast.SchemaNode>()
133
+ const enumNames: Array<string> = []
134
+ const discriminatorParentNodes: Array<ast.SchemaNode> = []
135
+
136
+ for (const [name, schema] of Object.entries(schemas)) {
137
+ const node = parseSchema({ schema, name }, parserOptions)
138
+ allNodes.push(node)
139
+ if (node.type === 'ref' && node.name && node.name !== name) {
140
+ refAliasMap.set(name, node)
141
+ }
142
+ if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) {
143
+ enumNames.push(node.name)
144
+ }
145
+ if (discriminator === 'inherit' && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
146
+ discriminatorParentNodes.push(node)
147
+ }
148
+ }
149
+
150
+ const circularNames = [...ast.findCircularSchemas(allNodes)]
151
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
152
+
153
+ let dedupePlan: ast.DedupePlan | null = null
154
+ if (dedupe) {
155
+ // One extra parse pass over operations so duplicates in request/response bodies are seen.
156
+ // Reuses the already-parsed `allNodes` for schemas — no second schema parse.
157
+ const operationNodes: Array<ast.OperationNode> = []
158
+ for (const methods of Object.values(baseOas.getPaths())) {
159
+ for (const operation of Object.values(methods)) {
160
+ if (!operation) continue
161
+ const operationNode = parseOperation(parserOptions, operation)
162
+ if (operationNode) operationNodes.push(operationNode)
163
+ }
164
+ }
165
+
166
+ dedupePlan = createDedupePlan({ schemaNodes: allNodes, operationNodes, schemaNames: Object.keys(schemas), circularNames })
167
+
168
+ for (const definition of dedupePlan.hoisted) {
169
+ if (definition.type === 'enum' && definition.name) enumNames.push(definition.name)
170
+ }
171
+ }
172
+
173
+ return { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan }
174
+ }
175
+
176
+ /**
177
+ * Creates a lazy `InputStreamNode` from already-resolved adapter state.
178
+ *
179
+ * The schema and operation iterables each start a fresh parse pass on every
180
+ * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
181
+ * stream object independently without sharing a cursor or holding all nodes in memory.
182
+ *
183
+ * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
184
+ * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
189
+ * for await (const schema of streamNode.schemas) {
190
+ * // each call to for-await restarts from the first schema
191
+ * }
192
+ * ```
193
+ */
194
+ export function createInputStream({
195
+ schemas,
196
+ parseSchema,
197
+ parseOperation,
198
+ baseOas,
199
+ parserOptions,
200
+ refAliasMap,
201
+ discriminatorChildMap,
202
+ dedupePlan,
203
+ meta,
204
+ }: {
205
+ schemas: Record<string, SchemaObject>
206
+ parseSchema: SchemaParser['parseSchema']
207
+ parseOperation: SchemaParser['parseOperation']
208
+ baseOas: BaseOas
209
+ parserOptions: ast.ParserOptions
210
+ refAliasMap: Map<string, ast.SchemaNode>
211
+ discriminatorChildMap: Map<string, DiscriminatorTarget> | null
212
+ dedupePlan: ast.DedupePlan | null
213
+ meta: ast.InputMeta
214
+ }): ast.InputStreamNode {
215
+ // Rewrites a top-level schema against the dedupe plan: a structurally identical sibling
216
+ // becomes a `ref` alias to the canonical one (keeping its own name); otherwise nested
217
+ // duplicates are collapsed while the schema's own root is preserved.
218
+ const rewriteTopLevelSchema = (node: ast.SchemaNode): ast.SchemaNode => {
219
+ if (!dedupePlan) return node
220
+
221
+ const canonical = dedupePlan.canonicalBySignature.get(ast.schemaSignature(node))
222
+ if (canonical && canonical.name !== node.name) {
223
+ return ast.createSchema({
224
+ type: 'ref',
225
+ name: node.name ?? null,
226
+ ref: canonical.ref,
227
+ description: node.description,
228
+ deprecated: node.deprecated,
229
+ })
230
+ }
231
+
232
+ return ast.applyDedupe(node, dedupePlan.canonicalBySignature, true)
233
+ }
234
+
235
+ const schemasIterable: AsyncIterable<ast.SchemaNode> = {
236
+ [Symbol.asyncIterator]() {
237
+ return (async function* () {
238
+ // Hoisted canonical definitions are emitted first so the schema list owns the shared shapes.
239
+ if (dedupePlan) {
240
+ for (const definition of dedupePlan.hoisted) yield definition
241
+ }
242
+
243
+ for (const [name, schema] of Object.entries(schemas)) {
244
+ // Inline ref aliases: replace the alias entry with its target's parsed node
245
+ // (keeping the alias name). Skip the first parse entirely for alias entries
246
+ // since that result is never used.
247
+ const alias = refAliasMap.get(name)
248
+ if (alias?.name && schemas[alias.name]) {
249
+ yield rewriteTopLevelSchema({ ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name })
250
+ continue
251
+ }
252
+
253
+ const parsed = parseSchema({ schema, name }, parserOptions)
254
+ const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
255
+ yield rewriteTopLevelSchema(node)
256
+ }
257
+ })()
258
+ },
259
+ }
260
+
261
+ const operationsIterable: AsyncIterable<ast.OperationNode> = {
262
+ [Symbol.asyncIterator]() {
263
+ return (async function* () {
264
+ for (const methods of Object.values(baseOas.getPaths())) {
265
+ for (const operation of Object.values(methods)) {
266
+ if (!operation) continue
267
+ const node = parseOperation(parserOptions, operation)
268
+ if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan.canonicalBySignature) : node
269
+ }
270
+ }
271
+ })()
272
+ },
273
+ }
274
+
275
+ return ast.createStreamInput(schemasIterable, operationsIterable, meta)
276
+ }
package/src/types.ts CHANGED
@@ -123,8 +123,9 @@ export type ResponseObject = OASResponseObject
123
123
  export type MediaTypeObject = OASMediaTypeObject
124
124
 
125
125
  /**
126
- * Configuration options for the OpenAPI adapter.
127
- * Controls spec validation, content-type selection, and server URL resolution.
126
+ * Configuration options for the OpenAPI adapter. Controls spec validation,
127
+ * content-type selection, server URL resolution, and how types are derived
128
+ * from the spec.
128
129
  *
129
130
  * @example
130
131
  * ```ts
@@ -138,23 +139,28 @@ export type MediaTypeObject = OASMediaTypeObject
138
139
  */
139
140
  export type AdapterOasOptions = {
140
141
  /**
141
- * Validate the OpenAPI spec before parsing.
142
+ * Validate the OpenAPI spec with `@readme/openapi-parser` before parsing.
143
+ * Set to `false` only when you have a known-invalid spec you still want to
144
+ * generate from.
145
+ *
142
146
  * @default true
143
147
  */
144
148
  validate?: boolean
145
149
  /**
146
- * Preferred content-type used when extracting request/response schemas.
147
- * Defaults to the first valid JSON media type found in the spec.
150
+ * Preferred media type when an operation defines several. Defaults to the
151
+ * first JSON-compatible media type found in the spec.
148
152
  */
149
153
  contentType?: ContentType
150
154
  /**
151
- * Index into `oas.api.servers` for computing `baseURL`.
152
- * `0` first server, `1` second server. Omit to leave `baseURL` undefined.
155
+ * Index into the `servers` array from your OpenAPI spec. Used to compute the
156
+ * base URL for plugins that need it. Most projects pick `0` for the primary
157
+ * server. Omit to leave `baseURL` undefined.
153
158
  */
154
159
  serverIndex?: number
155
160
  /**
156
161
  * Override values for `{variable}` placeholders in the selected server URL.
157
- * Only used when `serverIndex` is set.
162
+ * Only used when `serverIndex` is set. Variables you do not provide use
163
+ * their `default` value from the spec.
158
164
  *
159
165
  * @example
160
166
  * ```ts
@@ -165,12 +171,28 @@ export type AdapterOasOptions = {
165
171
  */
166
172
  serverVariables?: Record<string, string>
167
173
  /**
168
- * How the discriminator field is interpreted.
169
- * - `'strict'` uses `oneOf` schemas as written in the spec.
170
- * - `'inherit'` propagates discriminator values into child schemas from `discriminator.mapping`.
174
+ * How the `discriminator` field on `oneOf`/`anyOf` schemas is interpreted.
175
+ * - `'strict'` child schemas stay exactly as written; the discriminator
176
+ * narrows types at the call site but child shapes are not modified.
177
+ * - `'inherit'` — Kubb propagates the discriminator property as a literal
178
+ * value into each child schema, so each branch's discriminator field is
179
+ * precisely typed.
180
+ *
171
181
  * @default 'strict'
172
182
  */
173
183
  discriminator?: 'strict' | 'inherit'
184
+ /**
185
+ * Collapse structurally identical schemas and enums into a single shared definition.
186
+ *
187
+ * Duplicated inline shapes (especially enums repeated across many properties) are hoisted
188
+ * into one named schema; every other occurrence — and any structurally identical top-level
189
+ * component — becomes a `ref` to it. Equality is shape-only: documentation such as
190
+ * `description` and `example` is ignored. Enabled by default; set to `false` to keep every
191
+ * occurrence inline and produce byte-for-byte identical output to earlier versions.
192
+ *
193
+ * @default true
194
+ */
195
+ dedupe?: boolean
174
196
  } & Partial<ast.ParserOptions>
175
197
 
176
198
  /**
@@ -182,6 +204,7 @@ export type AdapterOasResolvedOptions = {
182
204
  serverIndex: AdapterOasOptions['serverIndex']
183
205
  serverVariables: AdapterOasOptions['serverVariables']
184
206
  discriminator: NonNullable<AdapterOasOptions['discriminator']>
207
+ dedupe: NonNullable<AdapterOasOptions['dedupe']>
185
208
  dateType: NonNullable<AdapterOasOptions['dateType']>
186
209
  integerType: NonNullable<AdapterOasOptions['integerType']>
187
210
  unknownType: NonNullable<AdapterOasOptions['unknownType']>