@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.cjs +262 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +262 -137
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +1 -1
- package/src/constants.ts +13 -14
- package/src/dedupe.ts +237 -0
- package/src/dialect.ts +10 -6
- package/src/discriminator.ts +1 -1
- package/src/factory.ts +1 -1
- package/src/mime.ts +2 -3
- package/src/operation.ts +2 -2
- package/src/parser.ts +48 -42
- package/src/refs.ts +4 -3
- package/src/resolvers.ts +13 -5
- package/src/stream.ts +19 -81
- package/src/types.ts +1 -1
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
|
|
63
|
-
* `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to
|
|
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`
|
|
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.
|
|
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 {
|
|
1
|
+
import { findCircularSchemas } from '@kubb/ast/utils'
|
|
2
2
|
import { ast } from '@kubb/core'
|
|
3
|
-
import {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
121
|
+
const circularSchemas = new Set(circularNames)
|
|
122
|
+
const usedNames = new Set(Object.keys(schemas))
|
|
167
123
|
|
|
168
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
|
221
|
+
* Populated once the adapter resolves a spec's schemas, on the first `stream()` or `parse()`.
|
|
222
222
|
*
|
|
223
223
|
* @example
|
|
224
224
|
* ```ts
|