@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-beta.14",
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.5",
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.14"
50
+ "@kubb/core": "5.0.0-beta.16"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
@@ -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 document: Document | undefined
18
+ let petStoreDoc: Document | undefined
18
19
 
19
- async function getDocument(): Promise<Document> {
20
- if (!document) {
21
- document = await parseDocument(petStorePath)
20
+ async function getPetStoreDocument(): Promise<Document> {
21
+ if (!petStoreDoc) {
22
+ petStoreDoc = await parseDocument(petStorePath)
22
23
  }
23
- return document
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 getDocument()
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 parseFromConfig(source)
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
- const inputNode = ast.createInput({
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 inputNode
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
  })
@@ -6,24 +6,17 @@ type DiscriminatorTarget = {
6
6
  }
7
7
 
8
8
  /**
9
- * Injects discriminator enum values into child schemas so they know which value identifies them.
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
- * Returns a new `InputNode` the original is never mutated.
16
- *
17
- * @example
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 applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
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 root.schemas) {
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
- const objectNode = ast.narrowSchema(node, 'object')
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
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
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
- * Memoising by `$ref` path reduces the overall work from O(2^depth) to O(N)
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.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
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(([_path, methods]) =>
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),