@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/src/resolvers.ts CHANGED
@@ -59,9 +59,9 @@ export function getSchemaType(format: string): ast.SchemaType | null {
59
59
  }
60
60
 
61
61
  /**
62
- * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
63
- * `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
64
- * base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
62
+ * Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
63
+ * `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
64
+ * the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
65
65
  * diagnostic in step with the parser as `formatMap` grows.
66
66
  */
67
67
  export function isHandledFormat(format: string): boolean {
@@ -86,7 +86,7 @@ export type OperationsOptions = {
86
86
  /**
87
87
  * Returns all parameters for an operation, merging path-level and operation-level entries.
88
88
  * Operation-level parameters override path-level ones with the same `in:name` key.
89
- * `$ref` parameters resolve via `dereferenceWithRef` for backward compatibility.
89
+ * Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
90
90
  *
91
91
  * @example
92
92
  * ```ts
@@ -220,7 +220,8 @@ export function getRequestSchema(document: Document, operation: Operation, optio
220
220
  type SchemaSourceMode = 'schemas' | 'responses' | 'requestBodies'
221
221
 
222
222
  /**
223
- * A schema annotated with its component section source and original name. Used by `resolveNameCollisions` for cross-source collision resolution.
223
+ * A schema annotated with its component section source and original name. `getSchemas` uses this
224
+ * to resolve name collisions across sources.
224
225
  */
225
226
  type SchemaWithMetadata = {
226
227
  schema: SchemaObject
@@ -234,6 +235,10 @@ export type GetSchemasOptions = {
234
235
 
235
236
  export type GetSchemasResult = {
236
237
  schemas: Record<string, SchemaObject>
238
+ /**
239
+ * Maps each original component pointer (`#/components/<source>/<name>`) to the
240
+ * collision-resolved unique name used as the key in `schemas`.
241
+ */
237
242
  nameMapping: Map<string, string>
238
243
  }
239
244
 
@@ -248,7 +253,10 @@ export type GetSchemasResult = {
248
253
  * ```ts
249
254
  * flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
250
255
  * // { type: 'object', properties: {}, description: 'A pet' }
256
+ * ```
251
257
  *
258
+ * @example
259
+ * ```ts
252
260
  * flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
253
261
  * // returned unchanged, contains a $ref
254
262
  * ```
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,53 +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
- }
19
-
20
- /**
21
- * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
22
- * single extra parse pass over operations (so duplicates in request/response bodies are seen).
23
- *
24
- * Only enums and objects are candidates, and object shapes that reference a circular schema are
25
- * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
26
- * name (collision-resolved against existing component names). Shapes without a name stay inline.
27
- */
28
- function createDedupePlan({
29
- schemaNodes,
30
- operationNodes,
31
- schemaNames,
32
- circularNames,
33
- }: {
34
- schemaNodes: Array<ast.SchemaNode>
35
- operationNodes: Array<ast.OperationNode>
36
- schemaNames: Array<string>
37
- circularNames: Array<string>
38
- }): ast.DedupePlan {
39
- const circularSchemas = new Set(circularNames)
40
- const usedNames = new Set(schemaNames)
41
-
42
- return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
43
- isCandidate: (node) => {
44
- if (node.type === 'enum') return true
45
- if (node.type !== 'object') return false
46
- // Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
47
- if (node.name && circularSchemas.has(node.name)) return false
48
- return !containsCircularRef(node, { circularSchemas })
49
- },
50
- nameFor: (node) => {
51
- const base = node.name
52
- if (!base) return null
53
-
54
- let name = base
55
- let counter = 2
56
- while (usedNames.has(name)) {
57
- name = `${base}${counter++}`
58
- }
59
- usedNames.add(name)
60
- return name
61
- },
62
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
63
- })
18
+ dedupePlan: Plan | null
64
19
  }
65
20
 
66
21
  /**
@@ -153,7 +108,7 @@ export function preScan({
153
108
  const circularNames = [...findCircularSchemas(allNodes)]
154
109
  const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
155
110
 
156
- let dedupePlan: ast.DedupePlan | null = null
111
+ let dedupePlan: Plan | null = null
157
112
  if (dedupe) {
158
113
  // One extra parse pass over operations so duplicates in request/response bodies are seen.
159
114
  // Reuses the already-parsed `allNodes` for schemas, no second schema parse.
@@ -163,17 +118,19 @@ export function preScan({
163
118
  if (operationNode) operationNodes.push(operationNode)
164
119
  }
165
120
 
166
- dedupePlan = createDedupePlan({ schemaNodes: allNodes, operationNodes, schemaNames: Object.keys(schemas), circularNames })
121
+ const circularSchemas = new Set(circularNames)
122
+ const usedNames = new Set(Object.keys(schemas))
167
123
 
168
- for (const definition of dedupePlan.hoisted) {
124
+ dedupePlan = oasDialect.dedupe.plan([...allNodes, ...operationNodes], { circularSchemas, usedNames })
125
+
126
+ for (const definition of dedupePlan.extracted) {
169
127
  if (definition.type === 'enum' && definition.name) enumNames.push(definition.name)
170
128
  }
171
129
  }
172
130
 
173
131
  // Enum names that duplicate an earlier schema's content are never emitted, so they are not
174
132
  // advertised to plugins either.
175
- const aliasNames = dedupePlan?.aliasNames
176
- const emittedEnumNames = aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames
133
+ const emittedEnumNames = dedupePlan ? enumNames.filter((name) => !dedupePlan.isAlias(name)) : enumNames
177
134
 
178
135
  return { refAliasMap, enumNames: emittedEnumNames, circularNames, discriminatorChildMap, dedupePlan }
179
136
  }
@@ -214,55 +171,36 @@ export function createInputStream({
214
171
  parserOptions: ast.ParserOptions
215
172
  refAliasMap: Map<string, ast.SchemaNode>
216
173
  discriminatorChildMap: Map<string, DiscriminatorTarget> | null
217
- dedupePlan: ast.DedupePlan | null
174
+ dedupePlan: Plan | null
218
175
  meta: ast.InputMeta
219
176
  }): ast.InputNode<true> {
220
- // Rewrites a top-level schema against the dedupe plan: a structurally identical sibling
221
- // becomes a `ref` alias to the canonical one (keeping its own name). Otherwise nested
222
- // duplicates are collapsed while the schema's own root is preserved.
223
- const rewriteTopLevelSchema = (node: ast.SchemaNode): ast.SchemaNode => {
224
- if (!dedupePlan) return node
225
-
226
- const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node))
227
- if (canonical && canonical.name !== node.name) {
228
- return ast.factory.createSchema({
229
- type: 'ref',
230
- name: node.name ?? null,
231
- ref: canonical.ref,
232
- description: node.description,
233
- deprecated: node.deprecated,
234
- })
235
- }
236
-
237
- return ast.applyDedupe(node, dedupePlan, true)
238
- }
239
-
240
177
  const schemasIterable: AsyncIterable<ast.SchemaNode> = {
241
178
  [Symbol.asyncIterator]() {
242
179
  return (async function* () {
243
- // 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.
244
181
  if (dedupePlan) {
245
- for (const definition of dedupePlan.hoisted) yield definition
182
+ for (const definition of dedupePlan.extracted) yield definition
246
183
  }
247
184
 
248
185
  for (const [name, schema] of Object.entries(schemas)) {
249
186
  // A top-level schema whose content duplicates an earlier one is not emitted: every
250
187
  // ref to it is repointed at the first schema with that content, so its model would
251
188
  // be dead code.
252
- if (dedupePlan?.aliasNames.has(name)) continue
189
+ if (dedupePlan?.isAlias(name)) continue
253
190
 
254
191
  // Inline ref aliases: replace the alias entry with its target's parsed node
255
192
  // (keeping the alias name). Skip the first parse entirely for alias entries
256
193
  // since that result is never used.
257
194
  const alias = refAliasMap.get(name)
258
195
  if (alias?.name && schemas[alias.name]) {
259
- 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
260
198
  continue
261
199
  }
262
200
 
263
201
  const parsed = parseSchema({ schema, name }, parserOptions)
264
202
  const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
265
- yield rewriteTopLevelSchema(node)
203
+ yield dedupePlan ? dedupePlan.applyTopLevel(node) : node
266
204
  }
267
205
  })()
268
206
  },
@@ -273,7 +211,7 @@ export function createInputStream({
273
211
  return (async function* () {
274
212
  for (const operation of getOperations(document)) {
275
213
  const node = parseOperation(parserOptions, operation)
276
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node
214
+ if (node) yield dedupePlan ? dedupePlan.apply(node) : node
277
215
  }
278
216
  })()
279
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