@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/README.md +98 -0
- package/dist/index.cjs +778 -417
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +37 -66
- package/dist/index.js +778 -414
- package/dist/index.js.map +1 -1
- package/extension.yaml +431 -0
- package/package.json +6 -4
- package/src/adapter.bench.ts +64 -0
- package/src/adapter.ts +90 -48
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +57 -40
- package/src/factory.ts +1 -4
- package/src/index.ts +2 -2
- package/src/parser.ts +219 -134
- package/src/refs.ts +16 -4
- package/src/resolvers.ts +50 -21
- package/src/stream.ts +175 -0
- package/src/types.ts +21 -11
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
154
|
+
const streamNode = await createStream(source)
|
|
89
155
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
package/src/discriminator.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
54
|
-
let objNode:
|
|
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') :
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
3
|
-
export { mergeDocuments
|
|
2
|
+
export type { ValidateDocumentOptions } from './factory.ts'
|
|
3
|
+
export { mergeDocuments } from './factory.ts'
|
|
4
4
|
export type {
|
|
5
5
|
AdapterOas,
|
|
6
6
|
AdapterOasOptions,
|