@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/src/stream.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { containsCircularRef, findCircularSchemas } from '@kubb/ast/utils'
1
+ import { findCircularSchemas } from '@kubb/ast/utils'
2
2
  import { ast } from '@kubb/core'
3
- import { SCHEMA_REF_PREFIX } from './constants.ts'
3
+ import type { Plan } from './dedupe.ts'
4
+ import { oasDialect } from './dialect.ts'
4
5
  import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
5
6
  import { getOperations } from './operation.ts'
6
7
  import type { SchemaParser } from './parser.ts'
@@ -14,7 +15,7 @@ export type PreScanResult = {
14
15
  enumNames: Array<string>
15
16
  circularNames: Array<string>
16
17
  discriminatorChildMap: Map<string, DiscriminatorTarget> | null
17
- dedupePlan: ast.DedupePlan | null
18
+ dedupePlan: Plan | null
18
19
  }
19
20
 
20
21
  /**
@@ -107,7 +108,7 @@ export function preScan({
107
108
  const circularNames = [...findCircularSchemas(allNodes)]
108
109
  const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
109
110
 
110
- let dedupePlan: ast.DedupePlan | null = null
111
+ let dedupePlan: Plan | null = null
111
112
  if (dedupe) {
112
113
  // One extra parse pass over operations so duplicates in request/response bodies are seen.
113
114
  // Reuses the already-parsed `allNodes` for schemas, no second schema parse.
@@ -117,44 +118,19 @@ export function preScan({
117
118
  if (operationNode) operationNodes.push(operationNode)
118
119
  }
119
120
 
120
- // Only enums and objects are candidates, and object shapes that reference a circular schema are
121
- // rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
122
- // name (collision-resolved against existing component names). Shapes without a name stay inline.
123
121
  const circularSchemas = new Set(circularNames)
124
122
  const usedNames = new Set(Object.keys(schemas))
125
123
 
126
- dedupePlan = ast.buildDedupePlan([...allNodes, ...operationNodes], {
127
- isCandidate: (node) => {
128
- if (node.type === 'enum') return true
129
- if (node.type !== 'object') return false
130
- // Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
131
- if (node.name && circularSchemas.has(node.name)) return false
132
- return !containsCircularRef(node, { circularSchemas })
133
- },
134
- nameFor: (node) => {
135
- const base = node.name
136
- if (!base) return null
124
+ dedupePlan = oasDialect.dedupe.plan([...allNodes, ...operationNodes], { circularSchemas, usedNames })
137
125
 
138
- let name = base
139
- let counter = 2
140
- while (usedNames.has(name)) {
141
- name = `${base}${counter++}`
142
- }
143
- usedNames.add(name)
144
- return name
145
- },
146
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
147
- })
148
-
149
- for (const definition of dedupePlan.hoisted) {
126
+ for (const definition of dedupePlan.extracted) {
150
127
  if (definition.type === 'enum' && definition.name) enumNames.push(definition.name)
151
128
  }
152
129
  }
153
130
 
154
131
  // Enum names that duplicate an earlier schema's content are never emitted, so they are not
155
132
  // advertised to plugins either.
156
- const aliasNames = dedupePlan?.aliasNames
157
- const emittedEnumNames = aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames
133
+ const emittedEnumNames = dedupePlan ? enumNames.filter((name) => !dedupePlan.isAlias(name)) : enumNames
158
134
 
159
135
  return { refAliasMap, enumNames: emittedEnumNames, circularNames, discriminatorChildMap, dedupePlan }
160
136
  }
@@ -195,55 +171,36 @@ export function createInputStream({
195
171
  parserOptions: ast.ParserOptions
196
172
  refAliasMap: Map<string, ast.SchemaNode>
197
173
  discriminatorChildMap: Map<string, DiscriminatorTarget> | null
198
- dedupePlan: ast.DedupePlan | null
174
+ dedupePlan: Plan | null
199
175
  meta: ast.InputMeta
200
176
  }): ast.InputNode<true> {
201
- // Rewrites a top-level schema against the dedupe plan: a structurally identical sibling
202
- // becomes a `ref` alias to the canonical one (keeping its own name). Otherwise nested
203
- // duplicates are collapsed while the schema's own root is preserved.
204
- const rewriteTopLevelSchema = (node: ast.SchemaNode): ast.SchemaNode => {
205
- if (!dedupePlan) return node
206
-
207
- const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node))
208
- if (canonical && canonical.name !== node.name) {
209
- return ast.factory.createSchema({
210
- type: 'ref',
211
- name: node.name ?? null,
212
- ref: canonical.ref,
213
- description: node.description,
214
- deprecated: node.deprecated,
215
- })
216
- }
217
-
218
- return ast.applyDedupe(node, dedupePlan, true)
219
- }
220
-
221
177
  const schemasIterable: AsyncIterable<ast.SchemaNode> = {
222
178
  [Symbol.asyncIterator]() {
223
179
  return (async function* () {
224
- // Hoisted canonical definitions are emitted first so the schema list owns the shared shapes.
180
+ // Extracted shared definitions are emitted first so the schema list owns the shared shapes.
225
181
  if (dedupePlan) {
226
- for (const definition of dedupePlan.hoisted) yield definition
182
+ for (const definition of dedupePlan.extracted) yield definition
227
183
  }
228
184
 
229
185
  for (const [name, schema] of Object.entries(schemas)) {
230
186
  // A top-level schema whose content duplicates an earlier one is not emitted: every
231
187
  // ref to it is repointed at the first schema with that content, so its model would
232
188
  // be dead code.
233
- if (dedupePlan?.aliasNames.has(name)) continue
189
+ if (dedupePlan?.isAlias(name)) continue
234
190
 
235
191
  // Inline ref aliases: replace the alias entry with its target's parsed node
236
192
  // (keeping the alias name). Skip the first parse entirely for alias entries
237
193
  // since that result is never used.
238
194
  const alias = refAliasMap.get(name)
239
195
  if (alias?.name && schemas[alias.name]) {
240
- yield rewriteTopLevelSchema({ ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name })
196
+ const aliasNode = { ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name }
197
+ yield dedupePlan ? dedupePlan.applyTopLevel(aliasNode) : aliasNode
241
198
  continue
242
199
  }
243
200
 
244
201
  const parsed = parseSchema({ schema, name }, parserOptions)
245
202
  const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
246
- yield rewriteTopLevelSchema(node)
203
+ yield dedupePlan ? dedupePlan.applyTopLevel(node) : node
247
204
  }
248
205
  })()
249
206
  },
@@ -254,7 +211,7 @@ export function createInputStream({
254
211
  return (async function* () {
255
212
  for (const operation of getOperations(document)) {
256
213
  const node = parseOperation(parserOptions, operation)
257
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node
214
+ if (node) yield dedupePlan ? dedupePlan.apply(node) : node
258
215
  }
259
216
  })()
260
217
  },
package/src/types.ts CHANGED
@@ -218,7 +218,7 @@ export type AdapterOasResolvedOptions = {
218
218
  enumSuffix: AdapterOasOptions['enumSuffix']
219
219
  /**
220
220
  * Map from original `$ref` paths to their collision-resolved schema names.
221
- * Populated after each `parse()` call.
221
+ * Populated once the adapter resolves a spec's schemas, on the first `stream()` or `parse()`.
222
222
  *
223
223
  * @example
224
224
  * ```ts