@kubb/adapter-oas 5.0.0-beta.19 → 5.0.0-beta.20

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.19",
3
+ "version": "5.0.0-beta.20",
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",
@@ -47,7 +47,7 @@
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.19"
50
+ "@kubb/core": "5.0.0-beta.20"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
@@ -48,10 +48,9 @@ describe.skipIf(!hasStripe)('Stripe spec — batch vs streaming (1,385 schemas)'
48
48
  )
49
49
 
50
50
  bench(
51
- 'streaming — adapter.count() + stream() drain',
51
+ 'streaming — adapter.stream() drain',
52
52
  async () => {
53
53
  const adapter = adapterOas({ validate: false })
54
- await adapter.count!(stripeSource)
55
54
  const stream = await adapter.stream!(stripeSource)
56
55
  for await (const _ of stream.schemas) {
57
56
  /* drain */
package/src/adapter.ts CHANGED
@@ -1,11 +1,12 @@
1
+ import { once } from '@internals/utils'
1
2
  import { ast, createAdapter } from '@kubb/core'
2
3
  import type { AdapterSource } from '@kubb/core'
3
4
  import BaseOas from 'oas'
4
5
  import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
5
- import { applyDiscriminatorInheritance, buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
6
6
  import { parseDocument, parseFromConfig, validateDocument } from './factory.ts'
7
- import { createSchemaParser, parseOas } from './parser.ts'
8
- import { getSchemas, resolveServerUrl } from './resolvers.ts'
7
+ import { createSchemaParser } from './parser.ts'
8
+ import { getSchemas } from './resolvers.ts'
9
+ import { createInputStream, preScan, resolveBaseUrl } from './stream.ts'
9
10
  import type { AdapterOas, Document } from './types.ts'
10
11
 
11
12
  /**
@@ -57,40 +58,52 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
57
58
 
58
59
  let nameMapping = new Map<string, string>()
59
60
  let parsedDocument: Document | null = null
60
- let schemaObjects: ReturnType<typeof getSchemas>['schemas'] | null = null
61
- let baseOasInstance: BaseOas | null = null
62
- let schemaParserInstance: ReturnType<typeof createSchemaParser> | null = null
63
61
 
64
- function resolveBaseURL(document: Document): string | undefined {
65
- const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
66
- return server?.url ? resolveServerUrl(server, serverVariables) : undefined
67
- }
68
-
69
- async function ensureDocument(source: AdapterSource): Promise<Document> {
70
- if (parsedDocument) return parsedDocument
62
+ // `once` collapses concurrent callers (e.g. a build's `stream()` racing with `openInStudio()`'s `parse()`) onto one in-flight promise.
63
+ const ensureDocument = once(async (source: AdapterSource): Promise<Document> => {
71
64
  const fresh = await parseFromConfig(source)
72
65
  if (validate) await validateDocument(fresh)
73
66
  parsedDocument = fresh
74
67
  return fresh
75
- }
76
-
77
- async function ensureSchemas(document: Document): Promise<ReturnType<typeof getSchemas>['schemas']> {
78
- if (!schemaObjects) {
79
- const result = getSchemas(document, { contentType })
80
- schemaObjects = result.schemas
81
- nameMapping = result.nameMapping
82
- }
83
- return schemaObjects
84
- }
85
-
86
- function ensureBaseOas(document: Document): BaseOas {
87
- if (!baseOasInstance) baseOasInstance = new BaseOas(document)
88
- return baseOasInstance
89
- }
90
-
91
- function ensureSchemaParser(document: Document): ReturnType<typeof createSchemaParser> {
92
- if (!schemaParserInstance) schemaParserInstance = createSchemaParser({ document, contentType })
93
- return schemaParserInstance
68
+ })
69
+
70
+ const ensureSchemas = once(async (document: Document) => {
71
+ const result = getSchemas(document, { contentType })
72
+ nameMapping = result.nameMapping
73
+ return result.schemas
74
+ })
75
+
76
+ const ensureBaseOas = once((document: Document) => new BaseOas(document))
77
+
78
+ const ensureSchemaParser = once((document: Document) => createSchemaParser({ document, contentType }))
79
+
80
+ const ensurePreScan = once((schemas: Awaited<ReturnType<typeof ensureSchemas>>, parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema']) =>
81
+ preScan({ schemas, parseSchema, parserOptions, discriminator }),
82
+ )
83
+
84
+ async function createStream(source: AdapterSource): Promise<ast.InputStreamNode> {
85
+ const document = await ensureDocument(source)
86
+ const schemas = await ensureSchemas(document)
87
+ const { parseSchema, parseOperation } = ensureSchemaParser(document)
88
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap } = ensurePreScan(schemas, parseSchema)
89
+
90
+ return createInputStream({
91
+ schemas,
92
+ parseSchema,
93
+ parseOperation,
94
+ baseOas: ensureBaseOas(document),
95
+ parserOptions,
96
+ refAliasMap,
97
+ discriminatorChildMap,
98
+ meta: {
99
+ title: document.info?.title,
100
+ description: document.info?.description,
101
+ version: document.info?.version,
102
+ baseURL: resolveBaseUrl({ document, serverIndex, serverVariables }),
103
+ circularNames,
104
+ enumNames,
105
+ },
106
+ })
94
107
  }
95
108
 
96
109
  return {
@@ -130,98 +143,18 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
130
143
  })
131
144
  },
132
145
  async parse(source) {
133
- const document = await ensureDocument(source)
134
-
135
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
136
- contentType,
137
- dateType,
138
- integerType,
139
- unknownType,
140
- emptySchemaType,
141
- enumSuffix,
142
- })
143
-
144
- const node = discriminator === 'inherit' ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot
145
-
146
- // This must happen after parseOas() because legacy enum remapping is finalized there.
147
- nameMapping = parsedNameMapping
148
-
149
- return ast.createInput({
150
- ...node,
151
- meta: {
152
- title: document.info?.title,
153
- description: document.info?.description,
154
- version: document.info?.version,
155
- baseURL: resolveBaseURL(document),
156
- },
157
- })
158
- },
159
- async count(source) {
160
- const document = await ensureDocument(source)
161
- const schemas = await ensureSchemas(document)
162
-
163
- const baseOas = ensureBaseOas(document)
164
- const operationCount = Object.values(baseOas.getPaths()).flatMap(Object.values).filter(Boolean).length
146
+ const streamNode = await createStream(source)
165
147
 
166
- return { schemas: Object.keys(schemas).length, operations: operationCount }
167
- },
168
- async stream(source) {
169
- const document = await ensureDocument(source)
170
- const schemas = await ensureSchemas(document)
171
-
172
- const discriminatorChildMap: Awaited<ReturnType<typeof buildDiscriminatorChildMap>> | null = (() => {
173
- if (discriminator !== 'inherit') return null
174
- const { parseSchema: _preParser } = ensureSchemaParser(document)
175
- const parentNodes: ast.SchemaNode[] = []
176
- for (const [name, schema] of Object.entries(schemas)) {
177
- if ((schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
178
- parentNodes.push(_preParser({ schema, name }, parserOptions))
179
- }
180
- }
181
- return parentNodes.length > 0 ? buildDiscriminatorChildMap(parentNodes) : null
182
- })()
183
-
184
- // Each [Symbol.asyncIterator]() call returns a fresh generator so multiple
185
- // plugins can do independent `for await` passes without shared state.
186
- // The underlying parser and BaseOas instance are cached at adapter scope.
187
- const schemasIterable: AsyncIterable<ast.SchemaNode> = {
188
- [Symbol.asyncIterator]() {
189
- return (async function* () {
190
- const { parseSchema: _parseSchema } = ensureSchemaParser(document)
191
- for (const [name, schema] of Object.entries(schemas)) {
192
- const parsedNode = _parseSchema({ schema, name }, parserOptions)
193
- const entry = discriminatorChildMap?.get(name)
194
- const node = entry ? patchDiscriminatorNode(parsedNode, entry) : parsedNode
195
- yield node
196
- }
197
- })()
198
- },
148
+ const collect = async <T>(iter: AsyncIterable<T>): Promise<T[]> => {
149
+ const out: T[] = []
150
+ for await (const item of iter) out.push(item)
151
+ return out
199
152
  }
200
153
 
201
- const operationsIterable: AsyncIterable<ast.OperationNode> = {
202
- [Symbol.asyncIterator]() {
203
- return (async function* () {
204
- const { parseOperation: _parseOperation } = ensureSchemaParser(document)
205
- const baseOas = ensureBaseOas(document)
206
- const paths = baseOas.getPaths()
207
-
208
- for (const methods of Object.values(paths)) {
209
- for (const operation of Object.values(methods)) {
210
- if (!operation) continue
211
- const node = _parseOperation(parserOptions, operation)
212
- if (node) yield node
213
- }
214
- }
215
- })()
216
- },
217
- }
154
+ const [schemas, operations] = await Promise.all([collect(streamNode.schemas), collect(streamNode.operations)])
218
155
 
219
- return ast.createStreamInput(schemasIterable, operationsIterable, {
220
- title: document.info?.title,
221
- description: document.info?.description,
222
- version: document.info?.version,
223
- baseURL: resolveBaseURL(document),
224
- })
156
+ return ast.createInput({ schemas, operations, meta: streamNode.meta })
225
157
  },
158
+ stream: createStream,
226
159
  }
227
160
  })
@@ -1,6 +1,7 @@
1
1
  import { ast } from '@kubb/core'
2
+ import type { SchemaNodeByType } from '@kubb/ast'
2
3
 
3
- type DiscriminatorTarget = {
4
+ export type DiscriminatorTarget = {
4
5
  propertyName: string
5
6
  enumValues: Array<string | number | boolean>
6
7
  }
@@ -43,8 +44,8 @@ export function buildDiscriminatorChildMap(schemas: ast.SchemaNode[]): Map<strin
43
44
  const intersectionNode = ast.narrowSchema(member, 'intersection')
44
45
  if (!intersectionNode?.members) continue
45
46
 
46
- let refNode: ReturnType<typeof ast.narrowSchema<'ref'>> | undefined
47
- let objNode: ReturnType<typeof ast.narrowSchema<'object'>> | undefined
47
+ let refNode: SchemaNodeByType['ref'] | undefined
48
+ let objNode: SchemaNodeByType['object'] | undefined
48
49
 
49
50
  for (const m of intersectionNode.members) {
50
51
  refNode ??= ast.narrowSchema(m, 'ref')
package/src/parser.ts CHANGED
@@ -30,6 +30,16 @@ export type OasParserContext = {
30
30
  contentType?: ContentType
31
31
  }
32
32
 
33
+ /**
34
+ * The object returned by {@link createSchemaParser}.
35
+ * Contains parser functions bound to a specific document.
36
+ */
37
+ export type SchemaParser = {
38
+ parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
39
+ parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
40
+ parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
41
+ }
42
+
33
43
  /**
34
44
  * Pre-computed per-schema context passed to every schema converter.
35
45
  *
package/src/stream.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { ast } from '@kubb/core'
2
+ import type BaseOas from 'oas'
3
+ import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
4
+ import type { SchemaParser } from './parser.ts'
5
+ import { resolveServerUrl } from './resolvers.ts'
6
+ import type { DiscriminatorTarget } from './discriminator.ts'
7
+ import type { AdapterOas, Document, SchemaObject } from './types.ts'
8
+
9
+ export type PreScanResult = {
10
+ refAliasMap: Map<string, ast.SchemaNode>
11
+ enumNames: string[]
12
+ circularNames: string[]
13
+ discriminatorChildMap: Map<string, DiscriminatorTarget> | null
14
+ }
15
+
16
+ /**
17
+ * Reads the server URL from the document's `servers` array at `serverIndex`,
18
+ * interpolating any `serverVariables` into the URL template.
19
+ *
20
+ * Returns `undefined` when `serverIndex` is omitted or out of range.
21
+ *
22
+ * @example Resolve the first server
23
+ * `resolveBaseUrl({ document, serverIndex: 0 })`
24
+ *
25
+ * @example Override a path variable
26
+ * `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
27
+ */
28
+ export function resolveBaseUrl({
29
+ document,
30
+ serverIndex,
31
+ serverVariables,
32
+ }: {
33
+ document: Document
34
+ serverIndex?: number
35
+ serverVariables?: Record<string, string>
36
+ }): string | undefined {
37
+ const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
38
+ return server?.url ? resolveServerUrl(server, serverVariables) : undefined
39
+ }
40
+
41
+ /**
42
+ * Parses every schema once to build the lookup structures that streaming needs upfront.
43
+ *
44
+ * Three things happen in this single pass:
45
+ * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
46
+ * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
47
+ * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
48
+ * The `allNodes` array is local and drops out of scope as soon as this function returns.
49
+ *
50
+ * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
51
+ * Both are proportional to the number of aliases or discriminator parents, not total schema count.
52
+ *
53
+ * Each schema is parsed again during the streaming pass — this is intentional.
54
+ * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const { refAliasMap, enumNames, circularNames } = preScan({
59
+ * schemas,
60
+ * parseSchema,
61
+ * parserOptions,
62
+ * discriminator: 'strict',
63
+ * })
64
+ * ```
65
+ */
66
+ export function preScan({
67
+ schemas,
68
+ parseSchema,
69
+ parserOptions,
70
+ discriminator,
71
+ }: {
72
+ schemas: Record<string, SchemaObject>
73
+ parseSchema: (entry: { schema: SchemaObject; name: string }, options: ast.ParserOptions) => ast.SchemaNode
74
+ parserOptions: ast.ParserOptions
75
+ discriminator: AdapterOas['options']['discriminator']
76
+ }): PreScanResult {
77
+ const allNodes: ast.SchemaNode[] = []
78
+ const refAliasMap = new Map<string, ast.SchemaNode>()
79
+ const enumNames: string[] = []
80
+ const discriminatorParentNodes: ast.SchemaNode[] = []
81
+
82
+ for (const [name, schema] of Object.entries(schemas)) {
83
+ const node = parseSchema({ schema, name }, parserOptions)
84
+ allNodes.push(node)
85
+ if (node.type === 'ref' && node.name && node.name !== name) {
86
+ refAliasMap.set(name, node)
87
+ }
88
+ if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) {
89
+ enumNames.push(node.name)
90
+ }
91
+ if (discriminator === 'inherit' && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
92
+ discriminatorParentNodes.push(node)
93
+ }
94
+ }
95
+
96
+ const circularNames = [...ast.findCircularSchemas(allNodes)]
97
+ const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
98
+
99
+ return { refAliasMap, enumNames, circularNames, discriminatorChildMap }
100
+ }
101
+
102
+ /**
103
+ * Creates a lazy `InputStreamNode` from already-resolved adapter state.
104
+ *
105
+ * The schema and operation iterables each start a fresh parse pass on every
106
+ * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
107
+ * stream object independently without sharing a cursor or holding all nodes in memory.
108
+ *
109
+ * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
110
+ * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
115
+ * for await (const schema of streamNode.schemas) {
116
+ * // each call to for-await restarts from the first schema
117
+ * }
118
+ * ```
119
+ */
120
+ export function createInputStream({
121
+ schemas,
122
+ parseSchema,
123
+ parseOperation,
124
+ baseOas,
125
+ parserOptions,
126
+ refAliasMap,
127
+ discriminatorChildMap,
128
+ meta,
129
+ }: {
130
+ schemas: Record<string, SchemaObject>
131
+ parseSchema: SchemaParser['parseSchema']
132
+ parseOperation: SchemaParser['parseOperation']
133
+ baseOas: BaseOas
134
+ parserOptions: ast.ParserOptions
135
+ refAliasMap: Map<string, ast.SchemaNode>
136
+ discriminatorChildMap: Map<string, DiscriminatorTarget> | null
137
+ meta: ast.InputMeta
138
+ }): ast.InputStreamNode {
139
+ const schemasIterable: AsyncIterable<ast.SchemaNode> = {
140
+ [Symbol.asyncIterator]() {
141
+ return (async function* () {
142
+ for (const [name, schema] of Object.entries(schemas)) {
143
+ // Inline ref aliases: replace the alias entry with its target's parsed node
144
+ // (keeping the alias name). Skip the first parse entirely for alias entries
145
+ // since that result is never used.
146
+ const alias = refAliasMap.get(name)
147
+ if (alias?.name && schemas[alias.name]) {
148
+ yield { ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name }
149
+ continue
150
+ }
151
+
152
+ const parsed = parseSchema({ schema, name }, parserOptions)
153
+ const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
154
+ yield node
155
+ }
156
+ })()
157
+ },
158
+ }
159
+
160
+ const operationsIterable: AsyncIterable<ast.OperationNode> = {
161
+ [Symbol.asyncIterator]() {
162
+ return (async function* () {
163
+ for (const methods of Object.values(baseOas.getPaths())) {
164
+ for (const operation of Object.values(methods)) {
165
+ if (!operation) continue
166
+ const node = parseOperation(parserOptions, operation)
167
+ if (node) yield node
168
+ }
169
+ }
170
+ })()
171
+ },
172
+ }
173
+
174
+ return ast.createStreamInput(schemasIterable, operationsIterable, meta)
175
+ }