@kubb/adapter-oas 5.0.0-beta.2 → 5.0.0-beta.21

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.
@@ -1,29 +1,23 @@
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
  }
7
8
 
8
9
  /**
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.
10
+ * Builds a map of child schema names discriminator patch data by scanning the given
11
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
14
12
  *
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
- * ```
13
+ * Extracted from `applyDiscriminatorInheritance` so the streaming path can call it on a
14
+ * small pre-parsed subset of schemas (only the discriminator parents) rather than on all
15
+ * schemas at once.
22
16
  */
23
- export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
17
+ export function buildDiscriminatorChildMap(schemas: ast.SchemaNode[]): Map<string, DiscriminatorTarget> {
24
18
  const childMap = new Map<string, DiscriminatorTarget>()
25
19
 
26
- for (const schema of root.schemas) {
20
+ for (const schema of schemas) {
27
21
  // Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
28
22
  // Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
29
23
  let unionNode = ast.narrowSchema(schema, 'union')
@@ -50,8 +44,8 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
50
44
  const intersectionNode = ast.narrowSchema(member, 'intersection')
51
45
  if (!intersectionNode?.members) continue
52
46
 
53
- let refNode: ReturnType<typeof ast.narrowSchema<'ref'>> | undefined
54
- let objNode: ReturnType<typeof ast.narrowSchema<'object'>> | undefined
47
+ let refNode: SchemaNodeByType['ref'] | null = null
48
+ let objNode: SchemaNodeByType['object'] | null = null
55
49
 
56
50
  for (const m of intersectionNode.members) {
57
51
  refNode ??= ast.narrowSchema(m, 'ref')
@@ -61,24 +55,61 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
61
55
  if (!refNode?.name || !objNode) continue
62
56
 
63
57
  const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
64
- const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : undefined
58
+ const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
65
59
  if (!enumNode?.enumValues?.length) continue
66
60
 
67
61
  const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
68
62
  if (!enumValues.length) continue
69
63
 
70
64
  const existing = childMap.get(refNode.name)
71
- if (existing) {
72
- existing.enumValues.push(...enumValues)
73
- } else {
74
- childMap.set(refNode.name, {
75
- propertyName: discriminatorPropertyName,
76
- enumValues: [...enumValues],
77
- })
65
+ if (!existing) {
66
+ childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
67
+ continue
78
68
  }
69
+ existing.enumValues.push(...enumValues)
79
70
  }
80
71
  }
81
72
 
73
+ return childMap
74
+ }
75
+
76
+ /**
77
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
78
+ * the discriminant property). Used by the streaming path to apply patches inline per yield
79
+ * without buffering all schemas.
80
+ */
81
+ export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
82
+ const objectNode = ast.narrowSchema(node, 'object')
83
+ if (!objectNode) return node
84
+
85
+ const { propertyName, enumValues } = entry
86
+ const enumSchema = ast.createSchema({ type: 'enum', enumValues })
87
+ const newProp = ast.createProperty({ name: propertyName, required: true, schema: enumSchema })
88
+
89
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
90
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
91
+
92
+ return { ...objectNode, properties: newProperties }
93
+ }
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
+
82
113
  if (childMap.size === 0) return root
83
114
 
84
115
  return ast.transform(root, {
@@ -88,21 +119,7 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
88
119
  const entry = childMap.get(node.name)
89
120
  if (!entry) return
90
121
 
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 }
122
+ return patchDiscriminatorNode(node, entry)
106
123
  },
107
124
  })
108
125
  }
package/src/factory.ts CHANGED
@@ -74,10 +74,7 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
74
74
  * ```
75
75
  */
76
76
  export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
77
- const documents: Document[] = []
78
- for (const p of pathOrApi) {
79
- documents.push(await parseDocument(p, { enablePaths: false, canBundle: false }))
80
- }
77
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
81
78
 
82
79
  if (documents.length === 0) {
83
80
  throw new Error('No OAS documents provided for merging.')
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { adapterOas, adapterOasName } from './adapter.ts'
2
- export type { ParseOptions, ValidateDocumentOptions } from './factory.ts'
3
- export { mergeDocuments, parseDocument, parseFromConfig, validateDocument } from './factory.ts'
2
+ export type { ValidateDocumentOptions } from './factory.ts'
3
+ export { mergeDocuments } from './factory.ts'
4
4
  export type {
5
5
  AdapterOas,
6
6
  AdapterOasOptions,
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
  *
@@ -76,9 +86,9 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
76
86
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
77
87
  * made possible by hoisting of function declarations.
78
88
  *
79
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
89
+ * @internal
80
90
  */
81
- function createSchemaParser(ctx: OasParserContext) {
91
+ export function createSchemaParser(ctx: OasParserContext) {
82
92
  const document = ctx.document
83
93
 
84
94
  // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
@@ -89,6 +99,20 @@ function createSchemaParser(ctx: OasParserContext) {
89
99
  */
90
100
  const resolvingRefs = new Set<string>()
91
101
 
102
+ /**
103
+ * Cache of already-resolved `$ref` schemas within this parser instance.
104
+ *
105
+ * Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
106
+ * every time it appears as a `$ref` in a different parent schema. In heavily
107
+ * cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
108
+ * blowup — `customer` alone may be referenced from dozens of top-level schemas,
109
+ * each triggering a fresh recursive expansion of its entire sub-tree.
110
+ *
111
+ * Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
112
+ * where N is the number of unique schema names.
113
+ */
114
+ const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
115
+
92
116
  /**
93
117
  * Converts a `$ref` schema into a `RefSchemaNode`.
94
118
  *
@@ -98,19 +122,23 @@ function createSchemaParser(ctx: OasParserContext) {
98
122
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
99
123
  */
100
124
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
101
- let resolvedSchema: ast.SchemaNode | undefined
125
+ let resolvedSchema: ast.SchemaNode | null = null
102
126
  const refPath = schema.$ref
103
127
  if (refPath && !resolvingRefs.has(refPath)) {
104
- try {
105
- const referenced = resolveRef<SchemaObject>(document, refPath)
106
- if (referenced) {
107
- resolvingRefs.add(refPath)
108
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
109
- resolvingRefs.delete(refPath)
128
+ if (!resolvedRefCache.has(refPath)) {
129
+ try {
130
+ const referenced = resolveRef<SchemaObject>(document, refPath)
131
+ if (referenced) {
132
+ resolvingRefs.add(refPath)
133
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
134
+ resolvingRefs.delete(refPath)
135
+ }
136
+ } catch {
137
+ // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
110
138
  }
111
- } catch {
112
- // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
139
+ resolvedRefCache.set(refPath, resolvedSchema)
113
140
  }
141
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null
114
142
  }
115
143
 
116
144
  return ast.createSchema({
@@ -150,6 +178,7 @@ function createSchemaParser(ctx: OasParserContext) {
150
178
  default: mergedDefault,
151
179
  example: schema.example ?? memberNode.example,
152
180
  pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
181
+ format: schema.format ?? memberNode.format,
153
182
  } as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
154
183
  }
155
184
 
@@ -226,7 +255,7 @@ function createSchemaParser(ctx: OasParserContext) {
226
255
 
227
256
  return ast.createSchema({
228
257
  type: 'intersection',
229
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
258
+ members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
230
259
  ...buildSchemaNode(schema, name, nullable, defaultValue),
231
260
  })
232
261
  }
@@ -340,6 +369,7 @@ function createSchemaParser(ctx: OasParserContext) {
340
369
  title: schema.title,
341
370
  description: schema.description,
342
371
  deprecated: schema.deprecated,
372
+ format: schema.format,
343
373
  })
344
374
  }
345
375
 
@@ -470,6 +500,7 @@ function createSchemaParser(ctx: OasParserContext) {
470
500
  writeOnly: schema.writeOnly,
471
501
  default: enumDefault,
472
502
  example: schema.example,
503
+ format: schema.format,
473
504
  }
474
505
 
475
506
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
@@ -517,15 +548,17 @@ function createSchemaParser(ctx: OasParserContext) {
517
548
 
518
549
  const resolvedChildName = ast.childName(name, propName)
519
550
  const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
520
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix)
521
-
522
- const tupleNode = ast.narrowSchema(schemaNode, 'tuple')
523
- if (tupleNode?.items) {
524
- const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
525
- if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
526
- schemaNode = { ...tupleNode, items: namedItems }
551
+ const schemaNode = (() => {
552
+ const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
553
+ const tupleNode = ast.narrowSchema(node, 'tuple')
554
+ if (tupleNode?.items) {
555
+ const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
556
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
557
+ return { ...tupleNode, items: namedItems }
558
+ }
527
559
  }
528
- }
560
+ return node
561
+ })()
529
562
 
530
563
  return ast.createProperty({
531
564
  name: propName,
@@ -539,18 +572,15 @@ function createSchemaParser(ctx: OasParserContext) {
539
572
  : []
540
573
 
541
574
  const additionalProperties = schema.additionalProperties
542
- let additionalPropertiesNode: ast.SchemaNode | boolean | undefined
543
- if (additionalProperties === true) {
544
- additionalPropertiesNode = true
545
- } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
546
- additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
547
- } else if (additionalProperties === false) {
548
- additionalPropertiesNode = false
549
- } else if (additionalProperties) {
550
- additionalPropertiesNode = ast.createSchema({
551
- type: typeOptionMap.get(options.unknownType)!,
552
- })
553
- }
575
+ const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
576
+ if (additionalProperties === true) return true
577
+ if (additionalProperties === false) return false
578
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) {
579
+ return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
580
+ }
581
+ if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
582
+ return undefined
583
+ })()
554
584
 
555
585
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
556
586
 
@@ -616,7 +646,7 @@ function createSchemaParser(ctx: OasParserContext) {
616
646
  */
617
647
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
618
648
  const rawItems = schema.items as SchemaObject | undefined
619
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(undefined, name, options.enumSuffix) : undefined
649
+ const itemName = rawItems?.enum?.length && name ? ast.enumPropName(null, name, options.enumSuffix) : undefined
620
650
  const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
621
651
 
622
652
  return ast.createSchema({
@@ -683,6 +713,7 @@ function createSchemaParser(ctx: OasParserContext) {
683
713
  description: schema.description,
684
714
  deprecated: schema.deprecated,
685
715
  nullable,
716
+ format: schema.format,
686
717
  })
687
718
  }
688
719
 
@@ -776,6 +807,7 @@ function createSchemaParser(ctx: OasParserContext) {
776
807
  name,
777
808
  title: schema.title,
778
809
  description: schema.description,
810
+ format: schema.format,
779
811
  })
780
812
  }
781
813
 
@@ -839,8 +871,8 @@ function createSchemaParser(ctx: OasParserContext) {
839
871
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
840
872
  * `$ref` entries are skipped since their flags live on the dereferenced target.
841
873
  */
842
- function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | undefined {
843
- if (!schema?.properties) return undefined
874
+ function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | null {
875
+ if (!schema?.properties) return null
844
876
 
845
877
  const keys: string[] = []
846
878
  for (const key in schema.properties) {
@@ -849,7 +881,7 @@ function createSchemaParser(ctx: OasParserContext) {
849
881
  keys.push(key)
850
882
  }
851
883
  }
852
- return keys.length ? keys : undefined
884
+ return keys.length ? keys : null
853
885
  }
854
886
 
855
887
  /**
@@ -990,7 +1022,7 @@ export function parseOas(
990
1022
  const baseOas = new BaseOas(document)
991
1023
  const paths = baseOas.getPaths()
992
1024
 
993
- const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
1025
+ const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
994
1026
  Object.entries(methods)
995
1027
  .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
996
1028
  .filter((op): op is ast.OperationNode => op !== null),
package/src/refs.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { isReference } from './guards.ts'
2
2
  import type { Document } from './types.ts'
3
3
 
4
+ const _refCache = new WeakMap<Document, Map<string, unknown>>()
5
+
4
6
  /**
5
7
  * Resolves a local JSON pointer reference from a document.
6
8
  *
@@ -18,11 +20,19 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
18
20
  if ($ref === '') {
19
21
  return null
20
22
  }
21
- if ($ref.startsWith('#')) {
22
- $ref = globalThis.decodeURIComponent($ref.substring(1))
23
- } else {
24
- return null
23
+ if (!$ref.startsWith('#')) return null
24
+ $ref = globalThis.decodeURIComponent($ref.substring(1))
25
+
26
+ let docCache = _refCache.get(document)
27
+ if (!docCache) {
28
+ docCache = new Map()
29
+ _refCache.set(document, docCache)
25
30
  }
31
+
32
+ if (docCache.has($ref)) {
33
+ return docCache.get($ref) as T
34
+ }
35
+
26
36
  const current = $ref
27
37
  .split('/')
28
38
  .filter(Boolean)
@@ -31,6 +41,8 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
31
41
  if (!current) {
32
42
  throw new Error(`Could not find a definition for ${origRef}.`)
33
43
  }
44
+
45
+ docCache.set($ref, current)
34
46
  return current as T
35
47
  }
36
48
 
package/src/resolvers.ts CHANGED
@@ -305,27 +305,25 @@ export function extractSchemaFromContent(content: Record<string, unknown> | unde
305
305
  /**
306
306
  * Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
307
307
  */
308
- function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
308
+ function* collectRefs(schema: unknown): Generator<string, void, undefined> {
309
309
  if (Array.isArray(schema)) {
310
- for (const item of schema) collectRefs(item, refs)
311
- return refs
310
+ for (const item of schema) yield* collectRefs(item)
311
+ return
312
312
  }
313
313
 
314
314
  if (schema && typeof schema === 'object') {
315
315
  for (const key in schema) {
316
316
  const value = (schema as Record<string, unknown>)[key]
317
- if (key === '$ref' && typeof value === 'string') {
318
- if (value.startsWith(SCHEMA_REF_PREFIX)) {
319
- const name = value.slice(SCHEMA_REF_PREFIX.length)
320
- if (name) refs.add(name)
321
- }
322
- } else {
323
- collectRefs(value, refs)
317
+ if (!(key === '$ref' && typeof value === 'string')) {
318
+ yield* collectRefs(value)
319
+ continue
320
+ }
321
+ if (value.startsWith(SCHEMA_REF_PREFIX)) {
322
+ const name = value.slice(SCHEMA_REF_PREFIX.length)
323
+ if (name) yield name
324
324
  }
325
325
  }
326
326
  }
327
-
328
- return refs
329
327
  }
330
328
 
331
329
  /**
@@ -344,7 +342,7 @@ export function sortSchemas(schemas: Record<string, SchemaObject>): Record<strin
344
342
  const deps = new Map<string, string[]>()
345
343
 
346
344
  for (const [name, schema] of Object.entries(schemas)) {
347
- deps.set(name, Array.from(collectRefs(schema)))
345
+ deps.set(name, [...new Set(collectRefs(schema))])
348
346
  }
349
347
 
350
348
  const sorted: string[] = []
@@ -514,6 +512,7 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
514
512
  writeOnly: schema.writeOnly,
515
513
  default: defaultValue,
516
514
  example: schema.example,
515
+ format: schema.format,
517
516
  } as const
518
517
  }
519
518
 
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 `null` 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 | null {
37
+ const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
38
+ return server?.url ? resolveServerUrl(server, serverVariables) : null
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
+ }