@kubb/adapter-oas 5.0.0-alpha.9 → 5.0.0-beta.2
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/dist/index.cjs +1299 -1242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +522 -144
- package/dist/index.js +1290 -1236
- package/dist/index.js.map +1 -1
- package/package.json +32 -35
- package/src/adapter.ts +82 -78
- package/src/constants.ts +77 -69
- package/src/discriminator.ts +108 -0
- package/src/factory.ts +165 -0
- package/src/guards.ts +68 -0
- package/src/index.ts +17 -0
- package/src/parser.ts +565 -655
- package/src/refs.ts +59 -0
- package/src/resolvers.ts +544 -0
- package/src/types.ts +172 -59
- package/src/oas/Oas.ts +0 -623
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -71
- package/src/oas/utils.ts +0 -402
- package/src/utils.ts +0 -168
package/package.json
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-
|
|
4
|
-
"description": "OpenAPI / Swagger adapter for Kubb
|
|
3
|
+
"version": "5.0.0-beta.2",
|
|
4
|
+
"description": "OpenAPI / Swagger adapter for Kubb converts OAS input into a @kubb/ast RootNode.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"openapi",
|
|
7
|
-
"swagger",
|
|
8
|
-
"oas",
|
|
9
6
|
"adapter",
|
|
10
|
-
"kubb",
|
|
11
7
|
"codegen",
|
|
8
|
+
"kubb",
|
|
9
|
+
"oas",
|
|
10
|
+
"openapi",
|
|
11
|
+
"swagger",
|
|
12
12
|
"typescript"
|
|
13
13
|
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "stijnvanhulle",
|
|
14
16
|
"repository": {
|
|
15
17
|
"type": "git",
|
|
16
18
|
"url": "git+https://github.com/kubb-labs/kubb.git",
|
|
17
19
|
"directory": "packages/adapter-oas"
|
|
18
20
|
},
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"dist",
|
|
24
|
+
"!/**/**.test.**",
|
|
25
|
+
"!/**/__tests__/**",
|
|
26
|
+
"!/**/__snapshots__/**"
|
|
27
|
+
],
|
|
22
28
|
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
23
33
|
"exports": {
|
|
24
34
|
".": {
|
|
25
35
|
"import": "./dist/index.js",
|
|
@@ -27,46 +37,33 @@
|
|
|
27
37
|
},
|
|
28
38
|
"./package.json": "./package.json"
|
|
29
39
|
},
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
"!/**/**.test.**",
|
|
35
|
-
"!/**/__tests__/**",
|
|
36
|
-
"!/**/__snapshots__/**"
|
|
37
|
-
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
38
44
|
"dependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"fflate": "^0.8.2",
|
|
43
|
-
"jsonpointer": "^5.0.1",
|
|
44
|
-
"oas": "^31.1.2",
|
|
45
|
-
"oas-normalize": "^16.0.2",
|
|
46
|
-
"openapi-types": "^12.1.3",
|
|
47
|
-
"remeda": "^2.33.6",
|
|
45
|
+
"@redocly/openapi-core": "^2.30.3",
|
|
46
|
+
"oas": "^32.1.18",
|
|
47
|
+
"oas-normalize": "^16.0.4",
|
|
48
48
|
"swagger2openapi": "^7.0.8",
|
|
49
|
-
"@kubb/
|
|
50
|
-
"@kubb/core": "5.0.0-alpha.9"
|
|
49
|
+
"@kubb/core": "5.0.0-beta.2"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
52
|
"@types/swagger2openapi": "^7.0.4",
|
|
53
|
+
"openapi-types": "^12.1.3",
|
|
54
54
|
"@internals/utils": "0.0.0"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=22"
|
|
58
58
|
},
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"registry": "https://registry.npmjs.org/"
|
|
59
|
+
"inlinedDependencies": {
|
|
60
|
+
"openapi-types": "12.1.3"
|
|
62
61
|
},
|
|
63
|
-
"main": "./dist/index.cjs",
|
|
64
|
-
"module": "./dist/index.js",
|
|
65
62
|
"scripts": {
|
|
66
63
|
"build": "tsdown",
|
|
67
64
|
"clean": "npx rimraf ./dist",
|
|
68
|
-
"lint": "
|
|
69
|
-
"lint:fix": "
|
|
65
|
+
"lint": "oxlint .",
|
|
66
|
+
"lint:fix": "oxlint --fix .",
|
|
70
67
|
"release": "pnpm publish --no-git-check",
|
|
71
68
|
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
|
|
72
69
|
"start": "tsdown --watch",
|
package/src/adapter.ts
CHANGED
|
@@ -1,122 +1,126 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import type { OasAdapter } from './types.ts'
|
|
9
|
-
import { getImports } from './utils.ts'
|
|
1
|
+
import { ast, createAdapter } from '@kubb/core'
|
|
2
|
+
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'
|
|
7
|
+
import type { AdapterOas, Document } from './types.ts'
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Stable string identifier for the OAS adapter used in Kubb's adapter registry.
|
|
11
|
+
*/
|
|
12
|
+
export const adapterOasName = 'oas' satisfies AdapterOas['name']
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Creates
|
|
15
|
+
* Creates the default OpenAPI / Swagger adapter for Kubb.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Parses the spec, optionally validates it, resolves the base URL, and converts
|
|
18
|
+
* everything into an `InputNode` that downstream plugins consume.
|
|
18
19
|
*
|
|
19
20
|
* @example
|
|
20
21
|
* ```ts
|
|
21
|
-
* import { defineConfig } from '
|
|
22
|
+
* import { defineConfig } from 'kubb'
|
|
22
23
|
* import { adapterOas } from '@kubb/adapter-oas'
|
|
24
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
23
25
|
*
|
|
24
26
|
* export default defineConfig({
|
|
25
|
-
* adapter: adapterOas({
|
|
26
|
-
* input:
|
|
27
|
-
* plugins: [pluginTs()
|
|
27
|
+
* adapter: adapterOas({ dateType: 'date', serverIndex: 0 }),
|
|
28
|
+
* input: { path: './openapi.yaml' },
|
|
29
|
+
* plugins: [pluginTs()],
|
|
28
30
|
* })
|
|
29
31
|
* ```
|
|
30
32
|
*/
|
|
31
|
-
export const adapterOas = createAdapter<
|
|
33
|
+
export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
32
34
|
const {
|
|
33
35
|
validate = true,
|
|
34
|
-
oasClass,
|
|
35
36
|
contentType,
|
|
36
37
|
serverIndex,
|
|
37
38
|
serverVariables,
|
|
38
39
|
discriminator = 'strict',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
emptySchemaType = unknownType,
|
|
40
|
+
dateType = DEFAULT_PARSER_OPTIONS.dateType,
|
|
41
|
+
integerType = DEFAULT_PARSER_OPTIONS.integerType,
|
|
42
|
+
unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
|
|
43
|
+
enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix,
|
|
44
|
+
emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
|
|
44
45
|
} = options
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Let-binding so parse() can replace it with a simple reassignment (no clear+loop).
|
|
48
|
+
let nameMapping = new Map<string, string>()
|
|
49
|
+
let parsedDocument: Document | null
|
|
50
|
+
let inputNode: ast.InputNode | null
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
53
|
name: adapterOasName,
|
|
52
|
-
options
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
get options() {
|
|
55
|
+
return {
|
|
56
|
+
validate,
|
|
57
|
+
contentType,
|
|
58
|
+
serverIndex,
|
|
59
|
+
serverVariables,
|
|
60
|
+
discriminator,
|
|
61
|
+
dateType,
|
|
62
|
+
integerType,
|
|
63
|
+
unknownType,
|
|
64
|
+
emptySchemaType,
|
|
65
|
+
enumSuffix,
|
|
66
|
+
nameMapping,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
get document() {
|
|
70
|
+
return parsedDocument
|
|
71
|
+
},
|
|
72
|
+
get inputNode() {
|
|
73
|
+
return inputNode
|
|
65
74
|
},
|
|
66
75
|
getImports(node, resolve) {
|
|
67
|
-
return
|
|
76
|
+
return ast.collectImports({
|
|
77
|
+
node,
|
|
78
|
+
nameMapping,
|
|
79
|
+
resolve: (schemaName) => {
|
|
80
|
+
const result = resolve(schemaName)
|
|
81
|
+
if (!result) return
|
|
82
|
+
|
|
83
|
+
return ast.createImport({ name: [result.name], path: result.path })
|
|
84
|
+
},
|
|
85
|
+
})
|
|
68
86
|
},
|
|
69
87
|
async parse(source) {
|
|
70
|
-
const
|
|
71
|
-
const oas = await parseFromConfig(fakeConfig, oasClass)
|
|
72
|
-
|
|
73
|
-
oas.setOptions({ contentType, discriminator, collisionDetection })
|
|
88
|
+
const document = await parseFromConfig(source)
|
|
74
89
|
|
|
75
90
|
if (validate) {
|
|
76
|
-
|
|
77
|
-
await oas.validate()
|
|
78
|
-
} catch (_err) {
|
|
79
|
-
// Validation failures are non-fatal — mirror plugin-oas behavior
|
|
80
|
-
}
|
|
91
|
+
await validateDocument(document)
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
const server = serverIndex !== undefined ?
|
|
94
|
+
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
84
95
|
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
85
96
|
|
|
86
|
-
const
|
|
97
|
+
const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
|
|
98
|
+
contentType,
|
|
99
|
+
dateType,
|
|
100
|
+
integerType,
|
|
101
|
+
unknownType,
|
|
102
|
+
emptySchemaType,
|
|
103
|
+
enumSuffix,
|
|
104
|
+
})
|
|
87
105
|
|
|
88
|
-
|
|
89
|
-
nameMapping.clear()
|
|
90
|
-
for (const [key, value] of parser.nameMapping) {
|
|
91
|
-
nameMapping.set(key, value)
|
|
92
|
-
}
|
|
106
|
+
const node = discriminator === 'inherit' ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot
|
|
93
107
|
|
|
94
|
-
|
|
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
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
...
|
|
113
|
+
inputNode = ast.createInput({
|
|
114
|
+
...node,
|
|
98
115
|
meta: {
|
|
99
|
-
title:
|
|
100
|
-
description:
|
|
101
|
-
version:
|
|
116
|
+
title: document.info?.title,
|
|
117
|
+
description: document.info?.description,
|
|
118
|
+
version: document.info?.version,
|
|
102
119
|
baseURL,
|
|
103
120
|
},
|
|
104
121
|
})
|
|
122
|
+
|
|
123
|
+
return inputNode
|
|
105
124
|
},
|
|
106
125
|
}
|
|
107
126
|
})
|
|
108
|
-
|
|
109
|
-
// TODO: remove once parseFromConfig accepts AdapterSource directly
|
|
110
|
-
function sourceToFakeConfig(source: AdapterSource): Parameters<typeof parseFromConfig>[0] {
|
|
111
|
-
switch (source.type) {
|
|
112
|
-
case 'path':
|
|
113
|
-
return { root: path.dirname(source.path), input: { path: source.path } } as Parameters<typeof parseFromConfig>[0]
|
|
114
|
-
case 'data':
|
|
115
|
-
return { root: process.cwd(), input: { data: source.data } } as Parameters<typeof parseFromConfig>[0]
|
|
116
|
-
case 'paths':
|
|
117
|
-
return {
|
|
118
|
-
root: source.paths[0] ? path.dirname(source.paths[0]) : process.cwd(),
|
|
119
|
-
input: source.paths.map((p) => ({ path: p })),
|
|
120
|
-
} as Parameters<typeof parseFromConfig>[0]
|
|
121
|
-
}
|
|
122
|
-
}
|
package/src/constants.ts
CHANGED
|
@@ -1,41 +1,83 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { HttpMethods as OASHttpMethods } from 'oas/types'
|
|
1
|
+
import { ast } from '@kubb/core'
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Default parser options applied when no explicit options are provided.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
9
|
+
*
|
|
10
|
+
* const parser = createOasParser(oas)
|
|
11
|
+
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_PARSER_OPTIONS = {
|
|
15
|
+
dateType: 'string',
|
|
16
|
+
integerType: 'bigint',
|
|
17
|
+
unknownType: 'any',
|
|
18
|
+
emptySchemaType: 'any',
|
|
19
|
+
enumSuffix: 'enum',
|
|
20
|
+
} as const satisfies ast.ParserOptions
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
|
|
24
|
+
*
|
|
25
|
+
* Used when building or parsing `$ref` strings.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const SCHEMA_REF_PREFIX = '#/components/schemas/' as const
|
|
5
33
|
|
|
6
34
|
/**
|
|
7
|
-
* OpenAPI version string written into
|
|
35
|
+
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
8
36
|
*/
|
|
9
37
|
export const MERGE_OPENAPI_VERSION = '3.0.0' as const
|
|
10
38
|
|
|
11
39
|
/**
|
|
12
|
-
* Fallback `info.title`
|
|
40
|
+
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
13
41
|
*/
|
|
14
42
|
export const MERGE_DEFAULT_TITLE = 'Merged API' as const
|
|
15
43
|
|
|
16
44
|
/**
|
|
17
|
-
* Fallback `info.version`
|
|
45
|
+
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
18
46
|
*/
|
|
19
47
|
export const MERGE_DEFAULT_VERSION = '1.0.0' as const
|
|
20
48
|
|
|
21
|
-
// ─── Schema analysis ───────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
49
|
/**
|
|
24
|
-
* JSON Schema keywords that
|
|
25
|
-
*
|
|
26
|
-
*
|
|
50
|
+
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
51
|
+
*
|
|
52
|
+
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
53
|
+
* intersection member rather than being merged into the parent.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
58
|
+
*
|
|
59
|
+
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
60
|
+
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
61
|
+
* ```
|
|
27
62
|
*/
|
|
28
63
|
export const structuralKeys = new Set(['properties', 'items', 'additionalProperties', 'oneOf', 'anyOf', 'allOf', 'not'] as const)
|
|
29
64
|
|
|
30
65
|
/**
|
|
31
|
-
*
|
|
66
|
+
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
67
|
+
*
|
|
68
|
+
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
69
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
|
|
70
|
+
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
|
|
71
|
+
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
32
72
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { formatMap } from '@kubb/adapter-oas'
|
|
36
76
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
77
|
+
* formatMap['uuid'] // 'uuid'
|
|
78
|
+
* formatMap['binary'] // 'blob'
|
|
79
|
+
* formatMap['float'] // 'number'
|
|
80
|
+
* ```
|
|
39
81
|
*/
|
|
40
82
|
export const formatMap = {
|
|
41
83
|
uuid: 'uuid',
|
|
@@ -44,8 +86,8 @@ export const formatMap = {
|
|
|
44
86
|
uri: 'url',
|
|
45
87
|
'uri-reference': 'url',
|
|
46
88
|
url: 'url',
|
|
47
|
-
ipv4: '
|
|
48
|
-
ipv6: '
|
|
89
|
+
ipv4: 'ipv4',
|
|
90
|
+
ipv6: 'ipv6',
|
|
49
91
|
hostname: 'url',
|
|
50
92
|
'idn-hostname': 'url',
|
|
51
93
|
binary: 'blob',
|
|
@@ -55,60 +97,26 @@ export const formatMap = {
|
|
|
55
97
|
int32: 'integer',
|
|
56
98
|
float: 'number',
|
|
57
99
|
double: 'number',
|
|
58
|
-
} as const satisfies Record<string, SchemaType>
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Exhaustive list of media types that Kubb recognizes.
|
|
62
|
-
*/
|
|
63
|
-
export const knownMediaTypes = new Set([
|
|
64
|
-
'application/json',
|
|
65
|
-
'application/xml',
|
|
66
|
-
'application/x-www-form-urlencoded',
|
|
67
|
-
'application/octet-stream',
|
|
68
|
-
'application/pdf',
|
|
69
|
-
'application/zip',
|
|
70
|
-
'application/graphql',
|
|
71
|
-
'multipart/form-data',
|
|
72
|
-
'text/plain',
|
|
73
|
-
'text/html',
|
|
74
|
-
'text/csv',
|
|
75
|
-
'text/xml',
|
|
76
|
-
'image/png',
|
|
77
|
-
'image/jpeg',
|
|
78
|
-
'image/gif',
|
|
79
|
-
'image/webp',
|
|
80
|
-
'image/svg+xml',
|
|
81
|
-
'audio/mpeg',
|
|
82
|
-
'video/mp4',
|
|
83
|
-
] as const)
|
|
100
|
+
} as const satisfies Record<string, ast.SchemaType>
|
|
84
101
|
|
|
85
102
|
/**
|
|
86
|
-
* Vendor extension keys
|
|
87
|
-
*
|
|
103
|
+
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
108
|
+
*
|
|
109
|
+
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
110
|
+
* ```
|
|
88
111
|
*/
|
|
89
112
|
export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
|
|
90
113
|
|
|
91
114
|
/**
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
export const SCALAR_PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'bigint', 'boolean'] as const)
|
|
95
|
-
|
|
96
|
-
// ─── HTTP ──────────────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Canonical HTTP method names for the Kubb OAS layer.
|
|
100
|
-
* Keys are uppercase (used in generated code); values are the lowercase strings
|
|
101
|
-
* that the `oas` library uses internally.
|
|
102
|
-
*
|
|
103
|
-
* TODO(v5): remove — use `httpMethods` from `@kubb/ast`
|
|
115
|
+
* Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
|
|
116
|
+
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
104
117
|
*/
|
|
105
|
-
export const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
DELETE: 'delete',
|
|
111
|
-
HEAD: 'head',
|
|
112
|
-
OPTIONS: 'options',
|
|
113
|
-
TRACE: 'trace',
|
|
114
|
-
} as const satisfies Record<Uppercase<OASHttpMethods>, OASHttpMethods>
|
|
118
|
+
export const typeOptionMap = new Map<'any' | 'unknown' | 'void', ast.ScalarSchemaType>([
|
|
119
|
+
['any', ast.schemaTypes.any],
|
|
120
|
+
['unknown', ast.schemaTypes.unknown],
|
|
121
|
+
['void', ast.schemaTypes.void],
|
|
122
|
+
])
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ast } from '@kubb/core'
|
|
2
|
+
|
|
3
|
+
type DiscriminatorTarget = {
|
|
4
|
+
propertyName: string
|
|
5
|
+
enumValues: Array<string | number | boolean>
|
|
6
|
+
}
|
|
7
|
+
|
|
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.
|
|
14
|
+
*
|
|
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
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
|
|
24
|
+
const childMap = new Map<string, DiscriminatorTarget>()
|
|
25
|
+
|
|
26
|
+
for (const schema of root.schemas) {
|
|
27
|
+
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
28
|
+
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
29
|
+
let unionNode = ast.narrowSchema(schema, 'union')
|
|
30
|
+
|
|
31
|
+
if (!unionNode) {
|
|
32
|
+
const intersectionMembers = ast.narrowSchema(schema, 'intersection')?.members
|
|
33
|
+
if (intersectionMembers) {
|
|
34
|
+
for (const m of intersectionMembers) {
|
|
35
|
+
const u = ast.narrowSchema(m, 'union')
|
|
36
|
+
if (u) {
|
|
37
|
+
unionNode = u
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
|
|
45
|
+
|
|
46
|
+
const { discriminatorPropertyName, members } = unionNode
|
|
47
|
+
|
|
48
|
+
for (const member of members) {
|
|
49
|
+
// Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
|
|
50
|
+
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
51
|
+
if (!intersectionNode?.members) continue
|
|
52
|
+
|
|
53
|
+
let refNode: ReturnType<typeof ast.narrowSchema<'ref'>> | undefined
|
|
54
|
+
let objNode: ReturnType<typeof ast.narrowSchema<'object'>> | undefined
|
|
55
|
+
|
|
56
|
+
for (const m of intersectionNode.members) {
|
|
57
|
+
refNode ??= ast.narrowSchema(m, 'ref')
|
|
58
|
+
objNode ??= ast.narrowSchema(m, 'object')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!refNode?.name || !objNode) continue
|
|
62
|
+
|
|
63
|
+
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
64
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : undefined
|
|
65
|
+
if (!enumNode?.enumValues?.length) continue
|
|
66
|
+
|
|
67
|
+
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
68
|
+
if (!enumValues.length) continue
|
|
69
|
+
|
|
70
|
+
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
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (childMap.size === 0) return root
|
|
83
|
+
|
|
84
|
+
return ast.transform(root, {
|
|
85
|
+
schema(node, { parent }) {
|
|
86
|
+
if (parent?.kind !== 'Input' || !node.name) return
|
|
87
|
+
|
|
88
|
+
const entry = childMap.get(node.name)
|
|
89
|
+
if (!entry) return
|
|
90
|
+
|
|
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 }
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
}
|