@kubb/adapter-oas 5.0.0-beta.3 → 5.0.0-beta.30

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/src/adapter.ts CHANGED
@@ -1,21 +1,27 @@
1
+ import { once } from '@internals/utils'
1
2
  import { ast, createAdapter } from '@kubb/core'
3
+ import type { AdapterSource } from '@kubb/core'
4
+ import BaseOas from 'oas'
2
5
  import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
3
- import { applyDiscriminatorInheritance } from './discriminator.ts'
4
- import { parseFromConfig, validateDocument } from './factory.ts'
5
- import { parseOas } from './parser.ts'
6
- import { resolveServerUrl } from './resolvers.ts'
6
+ import { parseDocument, parseFromConfig, validateDocument } from './factory.ts'
7
+ import { createSchemaParser } from './parser.ts'
8
+ import { getSchemas } from './resolvers.ts'
9
+ import { createInputStream, preScan, resolveBaseUrl } from './stream.ts'
7
10
  import type { AdapterOas, Document } from './types.ts'
8
11
 
9
12
  /**
10
- * Stable string identifier for the OAS adapter used in Kubb's adapter registry.
13
+ * Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
11
14
  */
12
15
  export const adapterOasName = 'oas' satisfies AdapterOas['name']
13
16
 
14
17
  /**
15
- * Creates the default OpenAPI / Swagger adapter for Kubb.
18
+ * Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
19
+ * file at `input.path`, validates it, resolves the base URL, and converts every
20
+ * schema and operation into the universal AST that every downstream plugin
21
+ * consumes.
16
22
  *
17
- * Parses the spec, optionally validates it, resolves the base URL, and converts
18
- * everything into an `InputNode` that downstream plugins consume.
23
+ * Configure once on `defineConfig`. The adapter's choices (date representation,
24
+ * integer width, server URL) apply to every plugin in the build.
19
25
  *
20
26
  * @example
21
27
  * ```ts
@@ -24,8 +30,13 @@ export const adapterOasName = 'oas' satisfies AdapterOas['name']
24
30
  * import { pluginTs } from '@kubb/plugin-ts'
25
31
  *
26
32
  * export default defineConfig({
27
- * adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
28
- * input: { path: './openapi.yaml' },
33
+ * input: { path: './petStore.yaml' },
34
+ * output: { path: './src/gen' },
35
+ * adapter: adapterOas({
36
+ * serverIndex: 0,
37
+ * discriminator: 'inherit',
38
+ * dateType: 'date',
39
+ * }),
29
40
  * plugins: [pluginTs()],
30
41
  * })
31
42
  * ```
@@ -44,10 +55,64 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
44
55
  emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
45
56
  } = options
46
57
 
47
- // Let-binding so parse() can replace it with a simple reassignment (no clear+loop).
58
+ const parserOptions: ast.ParserOptions = {
59
+ ...DEFAULT_PARSER_OPTIONS,
60
+ dateType,
61
+ integerType,
62
+ unknownType,
63
+ emptySchemaType,
64
+ enumSuffix,
65
+ }
66
+
48
67
  let nameMapping = new Map<string, string>()
49
- let parsedDocument: Document | null
50
- let inputNode: ast.InputNode | null
68
+ let parsedDocument: Document | null = null
69
+
70
+ // `once` collapses concurrent callers (e.g. a build's `stream()` racing with `openInStudio()`'s `parse()`) onto one in-flight promise.
71
+ const ensureDocument = once(async (source: AdapterSource): Promise<Document> => {
72
+ const fresh = await parseFromConfig(source)
73
+ if (validate) await validateDocument(fresh)
74
+ parsedDocument = fresh
75
+ return fresh
76
+ })
77
+
78
+ const ensureSchemas = once(async (document: Document) => {
79
+ const result = getSchemas(document, { contentType })
80
+ nameMapping = result.nameMapping
81
+ return result.schemas
82
+ })
83
+
84
+ const ensureBaseOas = once((document: Document) => new BaseOas(document))
85
+
86
+ const ensureSchemaParser = once((document: Document) => createSchemaParser({ document, contentType }))
87
+
88
+ const ensurePreScan = once((schemas: Awaited<ReturnType<typeof ensureSchemas>>, parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema']) =>
89
+ preScan({ schemas, parseSchema, parserOptions, discriminator }),
90
+ )
91
+
92
+ async function createStream(source: AdapterSource): Promise<ast.InputStreamNode> {
93
+ const document = await ensureDocument(source)
94
+ const schemas = await ensureSchemas(document)
95
+ const { parseSchema, parseOperation } = ensureSchemaParser(document)
96
+ const { refAliasMap, enumNames, circularNames, discriminatorChildMap } = ensurePreScan(schemas, parseSchema)
97
+
98
+ return createInputStream({
99
+ schemas,
100
+ parseSchema,
101
+ parseOperation,
102
+ baseOas: ensureBaseOas(document),
103
+ parserOptions,
104
+ refAliasMap,
105
+ discriminatorChildMap,
106
+ meta: {
107
+ title: document.info?.title,
108
+ description: document.info?.description,
109
+ version: document.info?.version,
110
+ baseURL: resolveBaseUrl({ document, serverIndex, serverVariables }),
111
+ circularNames,
112
+ enumNames,
113
+ },
114
+ })
115
+ }
51
116
 
52
117
  return {
53
118
  name: adapterOasName,
@@ -69,8 +134,9 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
69
134
  get document() {
70
135
  return parsedDocument
71
136
  },
72
- get inputNode() {
73
- return inputNode
137
+ async validate(input, options) {
138
+ const document = await parseDocument(input)
139
+ await validateDocument(document, options)
74
140
  },
75
141
  getImports(node, resolve) {
76
142
  return ast.collectImports({
@@ -78,49 +144,25 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
78
144
  nameMapping,
79
145
  resolve: (schemaName) => {
80
146
  const result = resolve(schemaName)
81
- if (!result) return
147
+ if (!result) return null
82
148
 
83
149
  return ast.createImport({ name: [result.name], path: result.path })
84
150
  },
85
151
  })
86
152
  },
87
153
  async parse(source) {
88
- const document = await parseFromConfig(source)
154
+ const streamNode = await createStream(source)
89
155
 
90
- if (validate) {
91
- await validateDocument(document)
156
+ const collect = async <T>(iter: AsyncIterable<T>): Promise<Array<T>> => {
157
+ const out: Array<T> = []
158
+ for await (const item of iter) out.push(item)
159
+ return out
92
160
  }
93
161
 
94
- const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
95
- const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
96
-
97
- const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
98
- contentType,
99
- dateType,
100
- integerType,
101
- unknownType,
102
- emptySchemaType,
103
- enumSuffix,
104
- })
105
-
106
- const node = discriminator === 'inherit' ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot
107
-
108
- // This must happen after parseOas() because legacy enum remapping is finalized there.
109
- nameMapping = parsedNameMapping
110
- // Expose the raw document so consumers (e.g. plugin-redoc) can access it.
111
- parsedDocument = document
112
-
113
- inputNode = ast.createInput({
114
- ...node,
115
- meta: {
116
- title: document.info?.title,
117
- description: document.info?.description,
118
- version: document.info?.version,
119
- baseURL,
120
- },
121
- })
162
+ const [schemas, operations] = await Promise.all([collect(streamNode.schemas), collect(streamNode.operations)])
122
163
 
123
- return inputNode
164
+ return ast.createInput({ schemas, operations, meta: streamNode.meta })
124
165
  },
166
+ stream: createStream,
125
167
  }
126
168
  })
package/src/dialect.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { ast } from '@kubb/core'
2
+ import { isDiscriminator, isNullable, isReference } from './guards.ts'
3
+ import { resolveRef } from './refs.ts'
4
+ import type { SchemaObject } from './types.ts'
5
+
6
+ /**
7
+ * The OpenAPI / Swagger dialect — the default used by `@kubb/adapter-oas`.
8
+ *
9
+ * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
10
+ * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
11
+ * ref resolution) so the converter pipeline and dispatch rules stay shared. A
12
+ * future adapter (e.g. AsyncAPI) ships its own dialect — `type: ['null', …]`
13
+ * nullability, no discriminator object, binary via `contentEncoding` — and reuses
14
+ * the rest unchanged.
15
+ *
16
+ * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
17
+ * JSON Schema vocabulary, so the converters keep that common case.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const parser = createSchemaParser(context) // uses oasDialect
22
+ * const parser = createSchemaParser(context, oasDialect) // explicit
23
+ * ```
24
+ */
25
+ export const oasDialect = ast.defineSchemaDialect({
26
+ name: 'oas',
27
+ isNullable,
28
+ isReference,
29
+ isDiscriminator,
30
+ isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
31
+ resolveRef,
32
+ })
33
+
34
+ /**
35
+ * The concrete dialect type for `@kubb/adapter-oas`. Keeps the OAS guard predicates
36
+ * (`isReference`, `isDiscriminator`) intact so converters narrow schemas after a check.
37
+ */
38
+ export type OasDialect = typeof oasDialect
@@ -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: Array<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,