@kubb/adapter-oas 5.0.0-beta.30 → 5.0.0-beta.32
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 +92 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +93 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +13 -4
- package/src/discriminator.ts +2 -35
- package/src/parser.ts +16 -0
- package/src/resolvers.ts +1 -8
- package/src/stream.ts +105 -4
- package/src/types.ts +13 -0
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.32",
|
|
4
4
|
"description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"registry": "https://registry.npmjs.org/"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@redocly/openapi-core": "^2.31.
|
|
46
|
+
"@redocly/openapi-core": "^2.31.4",
|
|
47
47
|
"oas": "^32.1.18",
|
|
48
48
|
"oas-normalize": "^16.0.4",
|
|
49
49
|
"swagger2openapi": "^7.0.8",
|
|
50
|
-
"@kubb/core": "5.0.0-beta.
|
|
50
|
+
"@kubb/core": "5.0.0-beta.32"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.ts
CHANGED
|
@@ -48,6 +48,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
48
48
|
serverIndex,
|
|
49
49
|
serverVariables,
|
|
50
50
|
discriminator = 'strict',
|
|
51
|
+
dedupe = true,
|
|
51
52
|
dateType = DEFAULT_PARSER_OPTIONS.dateType,
|
|
52
53
|
integerType = DEFAULT_PARSER_OPTIONS.integerType,
|
|
53
54
|
unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
|
|
@@ -85,24 +86,31 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
85
86
|
|
|
86
87
|
const ensureSchemaParser = once((document: Document) => createSchemaParser({ document, contentType }))
|
|
87
88
|
|
|
88
|
-
const ensurePreScan = once(
|
|
89
|
-
|
|
89
|
+
const ensurePreScan = once(
|
|
90
|
+
(
|
|
91
|
+
schemas: Awaited<ReturnType<typeof ensureSchemas>>,
|
|
92
|
+
parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema'],
|
|
93
|
+
parseOperation: ReturnType<typeof ensureSchemaParser>['parseOperation'],
|
|
94
|
+
baseOas: ReturnType<typeof ensureBaseOas>,
|
|
95
|
+
) => preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe }),
|
|
90
96
|
)
|
|
91
97
|
|
|
92
98
|
async function createStream(source: AdapterSource): Promise<ast.InputStreamNode> {
|
|
93
99
|
const document = await ensureDocument(source)
|
|
94
100
|
const schemas = await ensureSchemas(document)
|
|
95
101
|
const { parseSchema, parseOperation } = ensureSchemaParser(document)
|
|
96
|
-
const
|
|
102
|
+
const baseOas = ensureBaseOas(document)
|
|
103
|
+
const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(schemas, parseSchema, parseOperation, baseOas)
|
|
97
104
|
|
|
98
105
|
return createInputStream({
|
|
99
106
|
schemas,
|
|
100
107
|
parseSchema,
|
|
101
108
|
parseOperation,
|
|
102
|
-
baseOas
|
|
109
|
+
baseOas,
|
|
103
110
|
parserOptions,
|
|
104
111
|
refAliasMap,
|
|
105
112
|
discriminatorChildMap,
|
|
113
|
+
dedupePlan,
|
|
106
114
|
meta: {
|
|
107
115
|
title: document.info?.title,
|
|
108
116
|
description: document.info?.description,
|
|
@@ -123,6 +131,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
123
131
|
serverIndex,
|
|
124
132
|
serverVariables,
|
|
125
133
|
discriminator,
|
|
134
|
+
dedupe,
|
|
126
135
|
dateType,
|
|
127
136
|
integerType,
|
|
128
137
|
unknownType,
|
package/src/discriminator.ts
CHANGED
|
@@ -10,9 +10,8 @@ export type DiscriminatorTarget = {
|
|
|
10
10
|
* Builds a map of child schema names → discriminator patch data by scanning the given
|
|
11
11
|
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* schemas at once.
|
|
13
|
+
* The streaming path calls this on a small pre-parsed subset of schemas (only the
|
|
14
|
+
* discriminator parents) rather than on all schemas at once.
|
|
16
15
|
*/
|
|
17
16
|
export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
|
|
18
17
|
const childMap = new Map<string, DiscriminatorTarget>()
|
|
@@ -91,35 +90,3 @@ export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyNa
|
|
|
91
90
|
|
|
92
91
|
return { ...objectNode, properties: newProperties }
|
|
93
92
|
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Injects discriminator enum values into child schemas so they know which value identifies them.
|
|
97
|
-
*
|
|
98
|
-
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
99
|
-
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
100
|
-
* child object schema.
|
|
101
|
-
*
|
|
102
|
-
* Returns a new `InputNode` — the original is never mutated.
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```ts
|
|
106
|
-
* const { root } = parseOas(document, options)
|
|
107
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
|
|
111
|
-
const childMap = buildDiscriminatorChildMap(root.schemas)
|
|
112
|
-
|
|
113
|
-
if (childMap.size === 0) return root
|
|
114
|
-
|
|
115
|
-
return ast.transform(root, {
|
|
116
|
-
schema(node, { parent }) {
|
|
117
|
-
if (parent?.kind !== 'Input' || !node.name) return
|
|
118
|
-
|
|
119
|
-
const entry = childMap.get(node.name)
|
|
120
|
-
if (!entry) return
|
|
121
|
-
|
|
122
|
-
return patchDiscriminatorNode(node, entry)
|
|
123
|
-
},
|
|
124
|
-
})
|
|
125
|
-
}
|
package/src/parser.ts
CHANGED
|
@@ -494,6 +494,22 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
|
|
|
494
494
|
|
|
495
495
|
const nullInEnum = schema.enum!.includes(null)
|
|
496
496
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
497
|
+
|
|
498
|
+
// drf-spectacular `NullEnum` ({ enum: [null] }) is just `null`; an empty enum node would
|
|
499
|
+
// render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
|
|
500
|
+
// branch so it renders as a clean `null` (not `z.null().nullable()`).
|
|
501
|
+
if (nullInEnum && filteredValues.length === 0) {
|
|
502
|
+
return ast.createSchema({
|
|
503
|
+
type: 'null',
|
|
504
|
+
primitive: 'null',
|
|
505
|
+
name,
|
|
506
|
+
title: schema.title,
|
|
507
|
+
description: schema.description,
|
|
508
|
+
deprecated: schema.deprecated,
|
|
509
|
+
format: schema.format,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
497
513
|
const enumNullable = nullable || nullInEnum || undefined
|
|
498
514
|
const enumDefault = schema.default === null && enumNullable ? undefined : schema.default
|
|
499
515
|
const enumPrimitive = getPrimitiveType(type)
|
package/src/resolvers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pascalCase } from '@internals/utils'
|
|
2
|
-
import { ast } from '@kubb/core'
|
|
2
|
+
import type { ast } from '@kubb/core'
|
|
3
3
|
import type { ParameterObject, ServerObject } from 'oas/types'
|
|
4
4
|
import { isRef } from 'oas/types'
|
|
5
5
|
import { matchesMimeType } from 'oas/utils'
|
|
@@ -63,13 +63,6 @@ export function getPrimitiveType(type: string | undefined): ast.PrimitiveSchemaT
|
|
|
63
63
|
return 'string'
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Narrows a content-type string to the `MediaType` union Kubb recognizes, or returns `null`.
|
|
68
|
-
*/
|
|
69
|
-
export function getMediaType(contentType: string): ast.MediaType | null {
|
|
70
|
-
return Object.values(ast.mediaTypes).includes(contentType as ast.MediaType) ? (contentType as ast.MediaType) : null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
66
|
export type OperationsOptions = {
|
|
74
67
|
contentType?: ContentType
|
|
75
68
|
}
|
package/src/stream.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ast } from '@kubb/core'
|
|
2
2
|
import type BaseOas from 'oas'
|
|
3
|
+
import { SCHEMA_REF_PREFIX } from './constants.ts'
|
|
3
4
|
import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
|
|
4
5
|
import type { SchemaParser } from './parser.ts'
|
|
5
6
|
import { resolveServerUrl } from './resolvers.ts'
|
|
@@ -11,6 +12,53 @@ export type PreScanResult = {
|
|
|
11
12
|
enumNames: Array<string>
|
|
12
13
|
circularNames: Array<string>
|
|
13
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
|
+
})
|
|
14
62
|
}
|
|
15
63
|
|
|
16
64
|
/**
|
|
@@ -66,13 +114,19 @@ export function resolveBaseUrl({
|
|
|
66
114
|
export function preScan({
|
|
67
115
|
schemas,
|
|
68
116
|
parseSchema,
|
|
117
|
+
parseOperation,
|
|
118
|
+
baseOas,
|
|
69
119
|
parserOptions,
|
|
70
120
|
discriminator,
|
|
121
|
+
dedupe,
|
|
71
122
|
}: {
|
|
72
123
|
schemas: Record<string, SchemaObject>
|
|
73
124
|
parseSchema: (entry: { schema: SchemaObject; name: string }, options: ast.ParserOptions) => ast.SchemaNode
|
|
125
|
+
parseOperation: SchemaParser['parseOperation']
|
|
126
|
+
baseOas: BaseOas
|
|
74
127
|
parserOptions: ast.ParserOptions
|
|
75
128
|
discriminator: AdapterOas['options']['discriminator']
|
|
129
|
+
dedupe: boolean
|
|
76
130
|
}): PreScanResult {
|
|
77
131
|
const allNodes: Array<ast.SchemaNode> = []
|
|
78
132
|
const refAliasMap = new Map<string, ast.SchemaNode>()
|
|
@@ -96,7 +150,27 @@ export function preScan({
|
|
|
96
150
|
const circularNames = [...ast.findCircularSchemas(allNodes)]
|
|
97
151
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
|
|
98
152
|
|
|
99
|
-
|
|
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 }
|
|
100
174
|
}
|
|
101
175
|
|
|
102
176
|
/**
|
|
@@ -125,6 +199,7 @@ export function createInputStream({
|
|
|
125
199
|
parserOptions,
|
|
126
200
|
refAliasMap,
|
|
127
201
|
discriminatorChildMap,
|
|
202
|
+
dedupePlan,
|
|
128
203
|
meta,
|
|
129
204
|
}: {
|
|
130
205
|
schemas: Record<string, SchemaObject>
|
|
@@ -134,24 +209,50 @@ export function createInputStream({
|
|
|
134
209
|
parserOptions: ast.ParserOptions
|
|
135
210
|
refAliasMap: Map<string, ast.SchemaNode>
|
|
136
211
|
discriminatorChildMap: Map<string, DiscriminatorTarget> | null
|
|
212
|
+
dedupePlan: ast.DedupePlan | null
|
|
137
213
|
meta: ast.InputMeta
|
|
138
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
|
+
|
|
139
235
|
const schemasIterable: AsyncIterable<ast.SchemaNode> = {
|
|
140
236
|
[Symbol.asyncIterator]() {
|
|
141
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
|
+
|
|
142
243
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
143
244
|
// Inline ref aliases: replace the alias entry with its target's parsed node
|
|
144
245
|
// (keeping the alias name). Skip the first parse entirely for alias entries
|
|
145
246
|
// since that result is never used.
|
|
146
247
|
const alias = refAliasMap.get(name)
|
|
147
248
|
if (alias?.name && schemas[alias.name]) {
|
|
148
|
-
yield { ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name }
|
|
249
|
+
yield rewriteTopLevelSchema({ ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name })
|
|
149
250
|
continue
|
|
150
251
|
}
|
|
151
252
|
|
|
152
253
|
const parsed = parseSchema({ schema, name }, parserOptions)
|
|
153
254
|
const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
|
|
154
|
-
yield node
|
|
255
|
+
yield rewriteTopLevelSchema(node)
|
|
155
256
|
}
|
|
156
257
|
})()
|
|
157
258
|
},
|
|
@@ -164,7 +265,7 @@ export function createInputStream({
|
|
|
164
265
|
for (const operation of Object.values(methods)) {
|
|
165
266
|
if (!operation) continue
|
|
166
267
|
const node = parseOperation(parserOptions, operation)
|
|
167
|
-
if (node) yield node
|
|
268
|
+
if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan.canonicalBySignature) : node
|
|
168
269
|
}
|
|
169
270
|
}
|
|
170
271
|
})()
|
package/src/types.ts
CHANGED
|
@@ -181,6 +181,18 @@ export type AdapterOasOptions = {
|
|
|
181
181
|
* @default 'strict'
|
|
182
182
|
*/
|
|
183
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
|
|
184
196
|
} & Partial<ast.ParserOptions>
|
|
185
197
|
|
|
186
198
|
/**
|
|
@@ -192,6 +204,7 @@ export type AdapterOasResolvedOptions = {
|
|
|
192
204
|
serverIndex: AdapterOasOptions['serverIndex']
|
|
193
205
|
serverVariables: AdapterOasOptions['serverVariables']
|
|
194
206
|
discriminator: NonNullable<AdapterOasOptions['discriminator']>
|
|
207
|
+
dedupe: NonNullable<AdapterOasOptions['dedupe']>
|
|
195
208
|
dateType: NonNullable<AdapterOasOptions['dateType']>
|
|
196
209
|
integerType: NonNullable<AdapterOasOptions['integerType']>
|
|
197
210
|
unknownType: NonNullable<AdapterOasOptions['unknownType']>
|
|
File without changes
|