@kubb/adapter-oas 5.0.0-beta.14 → 5.0.0-beta.16
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 +153 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +152 -44
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.bench.ts +40 -10
- package/src/adapter.ts +108 -17
- package/src/discriminator.ts +48 -29
- package/src/parser.ts +5 -5
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.16",
|
|
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.30.
|
|
46
|
+
"@redocly/openapi-core": "^2.30.6",
|
|
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.16"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.bench.ts
CHANGED
|
@@ -1,35 +1,65 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
import { fileURLToPath } from 'node:url'
|
|
3
4
|
import { bench, describe } from 'vitest'
|
|
5
|
+
import { adapterOas } from './adapter.ts'
|
|
4
6
|
import { parseDocument } from './factory.ts'
|
|
5
7
|
import { parseOas } from './parser.ts'
|
|
6
8
|
import type { Document } from './types.ts'
|
|
9
|
+
import type { AdapterSource } from '@kubb/core'
|
|
7
10
|
|
|
8
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
12
|
const __dirname = path.dirname(__filename)
|
|
10
13
|
|
|
11
|
-
// TODO: replace with stripe.json once the parser handles its circular-reference
|
|
12
|
-
// expansion without exhausting heap (the full Stripe spec triggers exponential
|
|
13
|
-
// sub-tree duplication because resolvingRefs only prevents recursion within a
|
|
14
|
-
// single resolution chain, not repeated resolution of the same schema).
|
|
15
14
|
const petStorePath = path.resolve(__dirname, '../mocks/petStore.yaml')
|
|
15
|
+
const stripeSpecPath = '/tmp/kubb-stripe-spec3.json'
|
|
16
|
+
const hasStripe = existsSync(stripeSpecPath)
|
|
16
17
|
|
|
17
|
-
let
|
|
18
|
+
let petStoreDoc: Document | undefined
|
|
18
19
|
|
|
19
|
-
async function
|
|
20
|
-
if (!
|
|
21
|
-
|
|
20
|
+
async function getPetStoreDocument(): Promise<Document> {
|
|
21
|
+
if (!petStoreDoc) {
|
|
22
|
+
petStoreDoc = await parseDocument(petStorePath)
|
|
22
23
|
}
|
|
23
|
-
return
|
|
24
|
+
return petStoreDoc
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
describe('parseOas() performance', () => {
|
|
27
28
|
bench(
|
|
28
29
|
'petStore spec',
|
|
29
30
|
async () => {
|
|
30
|
-
const doc = await
|
|
31
|
+
const doc = await getPetStoreDocument()
|
|
31
32
|
parseOas(doc)
|
|
32
33
|
},
|
|
33
34
|
{ iterations: 5, warmupIterations: 1 },
|
|
34
35
|
)
|
|
35
36
|
})
|
|
37
|
+
|
|
38
|
+
describe.skipIf(!hasStripe)('Stripe spec — batch vs streaming (1,385 schemas)', () => {
|
|
39
|
+
const stripeSource: AdapterSource = { type: 'path', path: stripeSpecPath }
|
|
40
|
+
|
|
41
|
+
bench(
|
|
42
|
+
'batch — adapter.parse()',
|
|
43
|
+
async () => {
|
|
44
|
+
const adapter = adapterOas({ validate: false })
|
|
45
|
+
await adapter.parse(stripeSource)
|
|
46
|
+
},
|
|
47
|
+
{ iterations: 3, warmupIterations: 1 },
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
bench(
|
|
51
|
+
'streaming — adapter.count() + stream() drain',
|
|
52
|
+
async () => {
|
|
53
|
+
const adapter = adapterOas({ validate: false })
|
|
54
|
+
await adapter.count!(stripeSource)
|
|
55
|
+
const stream = await adapter.stream!(stripeSource)
|
|
56
|
+
for await (const _ of stream.schemas) {
|
|
57
|
+
/* drain */
|
|
58
|
+
}
|
|
59
|
+
for await (const _ of stream.operations) {
|
|
60
|
+
/* drain */
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ iterations: 3, warmupIterations: 1 },
|
|
64
|
+
)
|
|
65
|
+
})
|
package/src/adapter.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ast, createAdapter } from '@kubb/core'
|
|
2
|
+
import type { AdapterSource } from '@kubb/core'
|
|
3
|
+
import BaseOas from 'oas'
|
|
2
4
|
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
3
|
-
import { applyDiscriminatorInheritance } from './discriminator.ts'
|
|
5
|
+
import { applyDiscriminatorInheritance, buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
|
|
4
6
|
import { parseDocument, parseFromConfig, validateDocument } from './factory.ts'
|
|
5
|
-
import { parseOas } from './parser.ts'
|
|
6
|
-
import { resolveServerUrl } from './resolvers.ts'
|
|
7
|
+
import { createSchemaParser, parseOas } from './parser.ts'
|
|
8
|
+
import { getSchemas, resolveServerUrl } from './resolvers.ts'
|
|
7
9
|
import type { AdapterOas, Document } from './types.ts'
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -44,8 +46,40 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
44
46
|
emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
|
|
45
47
|
} = options
|
|
46
48
|
|
|
49
|
+
const parserOptions: ast.ParserOptions = {
|
|
50
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
51
|
+
dateType,
|
|
52
|
+
integerType,
|
|
53
|
+
unknownType,
|
|
54
|
+
emptySchemaType,
|
|
55
|
+
enumSuffix,
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
let nameMapping = new Map<string, string>()
|
|
48
|
-
let parsedDocument: Document | null
|
|
59
|
+
let parsedDocument: Document | null = null
|
|
60
|
+
let schemaObjects: ReturnType<typeof getSchemas>['schemas'] | null = null
|
|
61
|
+
|
|
62
|
+
function resolveBaseURL(document: Document): string | undefined {
|
|
63
|
+
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
64
|
+
return server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function ensureDocument(source: AdapterSource): Promise<Document> {
|
|
68
|
+
if (parsedDocument) return parsedDocument
|
|
69
|
+
const fresh = await parseFromConfig(source)
|
|
70
|
+
if (validate) await validateDocument(fresh)
|
|
71
|
+
parsedDocument = fresh
|
|
72
|
+
return fresh
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function ensureSchemas(document: Document): Promise<ReturnType<typeof getSchemas>['schemas']> {
|
|
76
|
+
if (!schemaObjects) {
|
|
77
|
+
const result = getSchemas(document, { contentType })
|
|
78
|
+
schemaObjects = result.schemas
|
|
79
|
+
nameMapping = result.nameMapping
|
|
80
|
+
}
|
|
81
|
+
return schemaObjects
|
|
82
|
+
}
|
|
49
83
|
|
|
50
84
|
return {
|
|
51
85
|
name: adapterOasName,
|
|
@@ -84,14 +118,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
84
118
|
})
|
|
85
119
|
},
|
|
86
120
|
async parse(source) {
|
|
87
|
-
const document = await
|
|
88
|
-
|
|
89
|
-
if (validate) {
|
|
90
|
-
await validateDocument(document)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
94
|
-
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
121
|
+
const document = await ensureDocument(source)
|
|
95
122
|
|
|
96
123
|
const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
|
|
97
124
|
contentType,
|
|
@@ -106,20 +133,84 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
106
133
|
|
|
107
134
|
// This must happen after parseOas() because legacy enum remapping is finalized there.
|
|
108
135
|
nameMapping = parsedNameMapping
|
|
109
|
-
// Expose the raw document so consumers (e.g. plugin-redoc) can access it.
|
|
110
|
-
parsedDocument = document
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
return ast.createInput({
|
|
113
138
|
...node,
|
|
114
139
|
meta: {
|
|
115
140
|
title: document.info?.title,
|
|
116
141
|
description: document.info?.description,
|
|
117
142
|
version: document.info?.version,
|
|
118
|
-
baseURL,
|
|
143
|
+
baseURL: resolveBaseURL(document),
|
|
119
144
|
},
|
|
120
145
|
})
|
|
146
|
+
},
|
|
147
|
+
async count(source) {
|
|
148
|
+
const document = await ensureDocument(source)
|
|
149
|
+
const schemas = await ensureSchemas(document)
|
|
150
|
+
|
|
151
|
+
const baseOas = new BaseOas(document)
|
|
152
|
+
const operationCount = Object.values(baseOas.getPaths()).flatMap(Object.values).filter(Boolean).length
|
|
121
153
|
|
|
122
|
-
return
|
|
154
|
+
return { schemas: Object.keys(schemas).length, operations: operationCount }
|
|
155
|
+
},
|
|
156
|
+
async stream(source) {
|
|
157
|
+
const document = await ensureDocument(source)
|
|
158
|
+
const schemas = await ensureSchemas(document)
|
|
159
|
+
|
|
160
|
+
let discriminatorChildMap: Awaited<ReturnType<typeof buildDiscriminatorChildMap>> | null = null
|
|
161
|
+
if (discriminator === 'inherit') {
|
|
162
|
+
const { parseSchema: _preParser } = createSchemaParser({ document, contentType })
|
|
163
|
+
const parentNodes: ast.SchemaNode[] = []
|
|
164
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
165
|
+
if ((schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
|
|
166
|
+
parentNodes.push(_preParser({ schema, name }, parserOptions))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (parentNodes.length > 0) {
|
|
170
|
+
discriminatorChildMap = buildDiscriminatorChildMap(parentNodes)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Each [Symbol.asyncIterator]() call returns a fresh generator so multiple
|
|
175
|
+
// plugins can do independent `for await` passes without shared state.
|
|
176
|
+
const schemasIterable: AsyncIterable<ast.SchemaNode> = {
|
|
177
|
+
[Symbol.asyncIterator]() {
|
|
178
|
+
return (async function* () {
|
|
179
|
+
const { parseSchema: _parseSchema } = createSchemaParser({ document, contentType })
|
|
180
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
181
|
+
let node = _parseSchema({ schema, name }, parserOptions)
|
|
182
|
+
const entry = discriminatorChildMap?.get(name)
|
|
183
|
+
if (entry) node = patchDiscriminatorNode(node, entry)
|
|
184
|
+
yield node
|
|
185
|
+
}
|
|
186
|
+
})()
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const operationsIterable: AsyncIterable<ast.OperationNode> = {
|
|
191
|
+
[Symbol.asyncIterator]() {
|
|
192
|
+
return (async function* () {
|
|
193
|
+
const { parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
|
|
194
|
+
const baseOas = new BaseOas(document)
|
|
195
|
+
const paths = baseOas.getPaths()
|
|
196
|
+
|
|
197
|
+
for (const methods of Object.values(paths)) {
|
|
198
|
+
for (const operation of Object.values(methods)) {
|
|
199
|
+
if (!operation) continue
|
|
200
|
+
const node = _parseOperation(parserOptions, operation)
|
|
201
|
+
if (node) yield node
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})()
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return ast.createStreamInput(schemasIterable, operationsIterable, {
|
|
209
|
+
title: document.info?.title,
|
|
210
|
+
description: document.info?.description,
|
|
211
|
+
version: document.info?.version,
|
|
212
|
+
baseURL: resolveBaseURL(document),
|
|
213
|
+
})
|
|
123
214
|
},
|
|
124
215
|
}
|
|
125
216
|
})
|
package/src/discriminator.ts
CHANGED
|
@@ -6,24 +6,17 @@ type DiscriminatorTarget = {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
12
|
-
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
13
|
-
* child object schema.
|
|
9
|
+
* Builds a map of child schema names → discriminator patch data by scanning the given
|
|
10
|
+
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* ```ts
|
|
19
|
-
* const { root } = parseOas(document, options)
|
|
20
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
21
|
-
* ```
|
|
12
|
+
* Extracted from `applyDiscriminatorInheritance` so the streaming path can call it on a
|
|
13
|
+
* small pre-parsed subset of schemas (only the discriminator parents) rather than on all
|
|
14
|
+
* schemas at once.
|
|
22
15
|
*/
|
|
23
|
-
export function
|
|
16
|
+
export function buildDiscriminatorChildMap(schemas: ast.SchemaNode[]): Map<string, DiscriminatorTarget> {
|
|
24
17
|
const childMap = new Map<string, DiscriminatorTarget>()
|
|
25
18
|
|
|
26
|
-
for (const schema of
|
|
19
|
+
for (const schema of schemas) {
|
|
27
20
|
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
28
21
|
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
29
22
|
let unionNode = ast.narrowSchema(schema, 'union')
|
|
@@ -79,6 +72,46 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
79
72
|
}
|
|
80
73
|
}
|
|
81
74
|
|
|
75
|
+
return childMap
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
80
|
+
* the discriminant property). Used by the streaming path to apply patches inline per yield
|
|
81
|
+
* without buffering all schemas.
|
|
82
|
+
*/
|
|
83
|
+
export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
|
|
84
|
+
const objectNode = ast.narrowSchema(node, 'object')
|
|
85
|
+
if (!objectNode) return node
|
|
86
|
+
|
|
87
|
+
const { propertyName, enumValues } = entry
|
|
88
|
+
const enumSchema = ast.createSchema({ type: 'enum', enumValues })
|
|
89
|
+
const newProp = ast.createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
90
|
+
|
|
91
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
92
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
93
|
+
|
|
94
|
+
return { ...objectNode, properties: newProperties }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Injects discriminator enum values into child schemas so they know which value identifies them.
|
|
99
|
+
*
|
|
100
|
+
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
101
|
+
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
102
|
+
* child object schema.
|
|
103
|
+
*
|
|
104
|
+
* Returns a new `InputNode` — the original is never mutated.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const { root } = parseOas(document, options)
|
|
109
|
+
* const next = applyDiscriminatorInheritance(root)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
|
|
113
|
+
const childMap = buildDiscriminatorChildMap(root.schemas)
|
|
114
|
+
|
|
82
115
|
if (childMap.size === 0) return root
|
|
83
116
|
|
|
84
117
|
return ast.transform(root, {
|
|
@@ -88,21 +121,7 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
88
121
|
const entry = childMap.get(node.name)
|
|
89
122
|
if (!entry) return
|
|
90
123
|
|
|
91
|
-
|
|
92
|
-
if (!objectNode) return
|
|
93
|
-
|
|
94
|
-
const { propertyName, enumValues } = entry
|
|
95
|
-
const enumSchema = ast.createSchema({ type: 'enum', enumValues })
|
|
96
|
-
const newProp = ast.createProperty({
|
|
97
|
-
name: propertyName,
|
|
98
|
-
required: true,
|
|
99
|
-
schema: enumSchema,
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
103
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
104
|
-
|
|
105
|
-
return { ...objectNode, properties: newProperties }
|
|
124
|
+
return patchDiscriminatorNode(node, entry)
|
|
106
125
|
},
|
|
107
126
|
})
|
|
108
127
|
}
|
package/src/parser.ts
CHANGED
|
@@ -76,9 +76,9 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
|
76
76
|
* Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
|
|
77
77
|
* made possible by hoisting of function declarations.
|
|
78
78
|
*
|
|
79
|
-
* @
|
|
79
|
+
* @internal
|
|
80
80
|
*/
|
|
81
|
-
function createSchemaParser(ctx: OasParserContext) {
|
|
81
|
+
export function createSchemaParser(ctx: OasParserContext) {
|
|
82
82
|
const document = ctx.document
|
|
83
83
|
|
|
84
84
|
// Branch handlers — each converts one OAS schema pattern to a SchemaNode.
|
|
@@ -98,7 +98,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
98
98
|
* blowup — `customer` alone may be referenced from dozens of top-level schemas,
|
|
99
99
|
* each triggering a fresh recursive expansion of its entire sub-tree.
|
|
100
100
|
*
|
|
101
|
-
*
|
|
101
|
+
* Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
|
|
102
102
|
* where N is the number of unique schema names.
|
|
103
103
|
*/
|
|
104
104
|
const resolvedRefCache = new Map<string, ast.SchemaNode | undefined>()
|
|
@@ -245,7 +245,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
245
245
|
|
|
246
246
|
return ast.createSchema({
|
|
247
247
|
type: 'intersection',
|
|
248
|
-
members: [...ast.
|
|
248
|
+
members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
|
|
249
249
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
250
250
|
})
|
|
251
251
|
}
|
|
@@ -1009,7 +1009,7 @@ export function parseOas(
|
|
|
1009
1009
|
const baseOas = new BaseOas(document)
|
|
1010
1010
|
const paths = baseOas.getPaths()
|
|
1011
1011
|
|
|
1012
|
-
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([
|
|
1012
|
+
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
|
|
1013
1013
|
Object.entries(methods)
|
|
1014
1014
|
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
1015
1015
|
.filter((op): op is ast.OperationNode => op !== null),
|