@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.61
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/LICENSE +17 -10
- package/README.md +100 -0
- package/dist/index.cjs +1459 -613
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +821 -63
- package/dist/index.js +1461 -610
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/src/adapter.bench.ts +60 -0
- package/src/adapter.ts +133 -56
- package/src/bundler.ts +71 -0
- package/src/constants.ts +17 -3
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +68 -44
- package/src/factory.ts +63 -48
- package/src/index.ts +0 -3
- package/src/mime.ts +22 -0
- package/src/operation.ts +194 -0
- package/src/parser.ts +335 -214
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +86 -48
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +283 -0
- package/src/types.ts +82 -53
- package/extension.yaml +0 -344
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "OpenAPI
|
|
3
|
+
"version": "5.0.0-beta.61",
|
|
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",
|
|
7
7
|
"codegen",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
23
23
|
"dist",
|
|
24
|
-
"extension.yaml",
|
|
25
24
|
"!/**/**.test.**",
|
|
26
25
|
"!/**/__tests__/**",
|
|
27
26
|
"!/**/__snapshots__/**"
|
|
@@ -43,13 +42,15 @@
|
|
|
43
42
|
"registry": "https://registry.npmjs.org/"
|
|
44
43
|
},
|
|
45
44
|
"dependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
48
|
-
"oas-normalize": "^16.0.4",
|
|
45
|
+
"@readme/openapi-parser": "^6.1.3",
|
|
46
|
+
"api-ref-bundler": "0.5.1",
|
|
49
47
|
"swagger2openapi": "^7.0.8",
|
|
50
|
-
"
|
|
48
|
+
"yaml": "^2.9.0",
|
|
49
|
+
"@kubb/ast": "5.0.0-beta.61",
|
|
50
|
+
"@kubb/core": "5.0.0-beta.61"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
+
"@types/json-schema": "^7.0.15",
|
|
53
54
|
"@types/swagger2openapi": "^7.0.4",
|
|
54
55
|
"openapi-types": "^12.1.3",
|
|
55
56
|
"@internals/utils": "0.0.0"
|
|
@@ -58,15 +59,18 @@
|
|
|
58
59
|
"node": ">=22"
|
|
59
60
|
},
|
|
60
61
|
"inlinedDependencies": {
|
|
62
|
+
"@types/json-schema": "7.0.15",
|
|
61
63
|
"openapi-types": "12.1.3"
|
|
62
64
|
},
|
|
63
65
|
"scripts": {
|
|
66
|
+
"bench": "vitest bench",
|
|
64
67
|
"build": "tsdown",
|
|
65
|
-
"clean": "
|
|
68
|
+
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
|
|
66
69
|
"lint": "oxlint .",
|
|
67
70
|
"lint:fix": "oxlint --fix .",
|
|
68
71
|
"release": "pnpm publish --no-git-check",
|
|
69
72
|
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
|
|
73
|
+
"release:stage": "pnpm stage publish --no-git-check",
|
|
70
74
|
"start": "tsdown --watch",
|
|
71
75
|
"test": "vitest --passWithNoTests",
|
|
72
76
|
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { bench, describe } from 'vitest'
|
|
4
|
+
import { adapterOas } from './adapter.ts'
|
|
5
|
+
import { parseDocument } from './factory.ts'
|
|
6
|
+
import { parseOas } from './parser.ts'
|
|
7
|
+
import type { Document } from './types.ts'
|
|
8
|
+
import type { AdapterSource } from '@kubb/core'
|
|
9
|
+
|
|
10
|
+
const petStorePath = path.resolve(import.meta.dirname, '../mocks/petStore.yaml')
|
|
11
|
+
const stripeSpecPath = '/tmp/kubb-stripe-spec3.json'
|
|
12
|
+
const hasStripe = existsSync(stripeSpecPath)
|
|
13
|
+
|
|
14
|
+
let petStoreDoc: Document | undefined
|
|
15
|
+
|
|
16
|
+
async function getPetStoreDocument(): Promise<Document> {
|
|
17
|
+
if (!petStoreDoc) {
|
|
18
|
+
petStoreDoc = await parseDocument(petStorePath)
|
|
19
|
+
}
|
|
20
|
+
return petStoreDoc
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('parseOas() performance', () => {
|
|
24
|
+
bench(
|
|
25
|
+
'petStore spec',
|
|
26
|
+
async () => {
|
|
27
|
+
const doc = await getPetStoreDocument()
|
|
28
|
+
parseOas(doc)
|
|
29
|
+
},
|
|
30
|
+
{ iterations: 5, warmupIterations: 1 },
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe.skipIf(!hasStripe)('Stripe spec — batch vs streaming (1,385 schemas)', () => {
|
|
35
|
+
const stripeSource: AdapterSource = { type: 'path', path: stripeSpecPath }
|
|
36
|
+
|
|
37
|
+
bench(
|
|
38
|
+
'batch — adapter.parse()',
|
|
39
|
+
async () => {
|
|
40
|
+
const adapter = adapterOas({ validate: false })
|
|
41
|
+
await adapter.parse(stripeSource)
|
|
42
|
+
},
|
|
43
|
+
{ iterations: 3, warmupIterations: 1 },
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
bench(
|
|
47
|
+
'streaming — adapter.stream() drain',
|
|
48
|
+
async () => {
|
|
49
|
+
const adapter = adapterOas({ validate: false })
|
|
50
|
+
const stream = await adapter.stream!(stripeSource)
|
|
51
|
+
for await (const _ of stream.schemas) {
|
|
52
|
+
/* drain */
|
|
53
|
+
}
|
|
54
|
+
for await (const _ of stream.operations) {
|
|
55
|
+
/* drain */
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{ iterations: 3, warmupIterations: 1 },
|
|
59
|
+
)
|
|
60
|
+
})
|
package/src/adapter.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { ast, createAdapter } from '@kubb/core'
|
|
2
|
+
import type { AdapterSource } from '@kubb/core'
|
|
2
3
|
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { assertInputExists, parseDocument, parseFromConfig, validateDocument } from './factory.ts'
|
|
5
|
+
import { createSchemaParser } from './parser.ts'
|
|
6
|
+
import { getSchemas } from './resolvers.ts'
|
|
7
|
+
import { createInputStream, preScan, resolveBaseUrl } from './stream.ts'
|
|
7
8
|
import type { AdapterOas, Document } from './types.ts'
|
|
9
|
+
import { collect, narrowSchema } from '@kubb/ast'
|
|
10
|
+
import { extractRefName } from '@kubb/ast/utils'
|
|
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
|
* ```
|
|
@@ -37,6 +48,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
37
48
|
serverIndex,
|
|
38
49
|
serverVariables,
|
|
39
50
|
discriminator = 'strict',
|
|
51
|
+
dedupe = true,
|
|
40
52
|
dateType = DEFAULT_PARSER_OPTIONS.dateType,
|
|
41
53
|
integerType = DEFAULT_PARSER_OPTIONS.integerType,
|
|
42
54
|
unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
|
|
@@ -44,10 +56,103 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
44
56
|
emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
|
|
45
57
|
} = options
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
const parserOptions: ast.ParserOptions = {
|
|
60
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
61
|
+
dateType,
|
|
62
|
+
integerType,
|
|
63
|
+
unknownType,
|
|
64
|
+
emptySchemaType,
|
|
65
|
+
enumSuffix,
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
let nameMapping = new Map<string, string>()
|
|
49
|
-
let parsedDocument: Document | null
|
|
50
|
-
|
|
69
|
+
let parsedDocument: Document | null = null
|
|
70
|
+
|
|
71
|
+
// Cache per source and per document so one adapter instance reused across a `defineConfig` array
|
|
72
|
+
// parses each config's spec instead of replaying the first one. Keying the document by its source
|
|
73
|
+
// object still collapses a config's concurrent `stream()` (build) and `parse()` (studio) calls,
|
|
74
|
+
// which share one source object, onto a single parse. The document-derived caches key off the
|
|
75
|
+
// resulting document, so distinct configs (distinct documents) stay isolated.
|
|
76
|
+
const documentCache = new WeakMap<AdapterSource, Promise<Document>>()
|
|
77
|
+
const schemasCache = new WeakMap<Document, Promise<ReturnType<typeof getSchemas>['schemas']>>()
|
|
78
|
+
const schemaParserCache = new WeakMap<Document, ReturnType<typeof createSchemaParser>>()
|
|
79
|
+
const preScanCache = new WeakMap<Document, ReturnType<typeof preScan>>()
|
|
80
|
+
|
|
81
|
+
function ensureDocument(source: AdapterSource): Promise<Document> {
|
|
82
|
+
const cached = documentCache.get(source)
|
|
83
|
+
if (cached) return cached
|
|
84
|
+
|
|
85
|
+
const promise = (async () => {
|
|
86
|
+
const fresh = await parseFromConfig(source)
|
|
87
|
+
if (validate) await validateDocument(fresh)
|
|
88
|
+
parsedDocument = fresh
|
|
89
|
+
return fresh
|
|
90
|
+
})()
|
|
91
|
+
documentCache.set(source, promise)
|
|
92
|
+
return promise
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureSchemas(document: Document): Promise<ReturnType<typeof getSchemas>['schemas']> {
|
|
96
|
+
const cached = schemasCache.get(document)
|
|
97
|
+
if (cached) return cached
|
|
98
|
+
|
|
99
|
+
const promise = Promise.resolve().then(() => {
|
|
100
|
+
const result = getSchemas(document, { contentType })
|
|
101
|
+
nameMapping = result.nameMapping
|
|
102
|
+
return result.schemas
|
|
103
|
+
})
|
|
104
|
+
schemasCache.set(document, promise)
|
|
105
|
+
return promise
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ensureSchemaParser(document: Document): ReturnType<typeof createSchemaParser> {
|
|
109
|
+
const cached = schemaParserCache.get(document)
|
|
110
|
+
if (cached) return cached
|
|
111
|
+
|
|
112
|
+
const parser = createSchemaParser({ document, contentType })
|
|
113
|
+
schemaParserCache.set(document, parser)
|
|
114
|
+
return parser
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensurePreScan(
|
|
118
|
+
document: Document,
|
|
119
|
+
schemas: ReturnType<typeof getSchemas>['schemas'],
|
|
120
|
+
parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema'],
|
|
121
|
+
parseOperation: ReturnType<typeof ensureSchemaParser>['parseOperation'],
|
|
122
|
+
): ReturnType<typeof preScan> {
|
|
123
|
+
const cached = preScanCache.get(document)
|
|
124
|
+
if (cached) return cached
|
|
125
|
+
|
|
126
|
+
const result = preScan({ schemas, parseSchema, parseOperation, document, parserOptions, discriminator, dedupe })
|
|
127
|
+
preScanCache.set(document, result)
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function createStream(source: AdapterSource): Promise<ast.InputNode<true>> {
|
|
132
|
+
const document = await ensureDocument(source)
|
|
133
|
+
const schemas = await ensureSchemas(document)
|
|
134
|
+
const { parseSchema, parseOperation } = ensureSchemaParser(document)
|
|
135
|
+
const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation)
|
|
136
|
+
|
|
137
|
+
return createInputStream({
|
|
138
|
+
schemas,
|
|
139
|
+
parseSchema,
|
|
140
|
+
parseOperation,
|
|
141
|
+
document,
|
|
142
|
+
parserOptions,
|
|
143
|
+
refAliasMap,
|
|
144
|
+
discriminatorChildMap,
|
|
145
|
+
dedupePlan,
|
|
146
|
+
meta: {
|
|
147
|
+
title: document.info?.title,
|
|
148
|
+
description: document.info?.description,
|
|
149
|
+
version: document.info?.version,
|
|
150
|
+
baseURL: resolveBaseUrl({ document, serverIndex, serverVariables }),
|
|
151
|
+
circularNames,
|
|
152
|
+
enumNames,
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
}
|
|
51
156
|
|
|
52
157
|
return {
|
|
53
158
|
name: adapterOasName,
|
|
@@ -58,6 +163,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
58
163
|
serverIndex,
|
|
59
164
|
serverVariables,
|
|
60
165
|
discriminator,
|
|
166
|
+
dedupe,
|
|
61
167
|
dateType,
|
|
62
168
|
integerType,
|
|
63
169
|
unknownType,
|
|
@@ -69,62 +175,33 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
69
175
|
get document() {
|
|
70
176
|
return parsedDocument
|
|
71
177
|
},
|
|
72
|
-
get inputNode() {
|
|
73
|
-
return inputNode
|
|
74
|
-
},
|
|
75
178
|
async validate(input, options) {
|
|
179
|
+
await assertInputExists(input)
|
|
76
180
|
const document = await parseDocument(input)
|
|
77
181
|
await validateDocument(document, options)
|
|
78
182
|
},
|
|
79
183
|
getImports(node, resolve) {
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
184
|
+
return collect(node, {
|
|
185
|
+
schema(schemaNode) {
|
|
186
|
+
const schemaRef = narrowSchema(schemaNode, 'ref')
|
|
187
|
+
if (!schemaRef?.ref) return null
|
|
188
|
+
|
|
189
|
+
const rawName = extractRefName(schemaRef.ref)
|
|
190
|
+
const schemaName = nameMapping.get(rawName) ?? rawName
|
|
84
191
|
const result = resolve(schemaName)
|
|
85
|
-
if (!result) return
|
|
192
|
+
if (!result) return null
|
|
86
193
|
|
|
87
|
-
return ast.createImport({ name: [result.name], path: result.path })
|
|
194
|
+
return ast.factory.createImport({ name: [result.name], path: result.path })
|
|
88
195
|
},
|
|
89
196
|
})
|
|
90
197
|
},
|
|
91
198
|
async parse(source) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (validate) {
|
|
95
|
-
await validateDocument(document)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
99
|
-
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
100
|
-
|
|
101
|
-
const { root: parsedRoot, nameMapping: parsedNameMapping } = parseOas(document, {
|
|
102
|
-
contentType,
|
|
103
|
-
dateType,
|
|
104
|
-
integerType,
|
|
105
|
-
unknownType,
|
|
106
|
-
emptySchemaType,
|
|
107
|
-
enumSuffix,
|
|
108
|
-
})
|
|
199
|
+
const streamNode = await createStream(source)
|
|
109
200
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
// This must happen after parseOas() because legacy enum remapping is finalized there.
|
|
113
|
-
nameMapping = parsedNameMapping
|
|
114
|
-
// Expose the raw document so consumers (e.g. plugin-redoc) can access it.
|
|
115
|
-
parsedDocument = document
|
|
116
|
-
|
|
117
|
-
inputNode = ast.createInput({
|
|
118
|
-
...node,
|
|
119
|
-
meta: {
|
|
120
|
-
title: document.info?.title,
|
|
121
|
-
description: document.info?.description,
|
|
122
|
-
version: document.info?.version,
|
|
123
|
-
baseURL,
|
|
124
|
-
},
|
|
125
|
-
})
|
|
201
|
+
const [schemas, operations] = await Promise.all([Array.fromAsync(streamNode.schemas), Array.fromAsync(streamNode.operations)])
|
|
126
202
|
|
|
127
|
-
return
|
|
203
|
+
return ast.factory.createInput({ schemas, operations, meta: streamNode.meta })
|
|
128
204
|
},
|
|
205
|
+
stream: createStream,
|
|
129
206
|
}
|
|
130
207
|
})
|
package/src/bundler.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { read } from '@internals/utils'
|
|
2
|
+
import { bundle } from 'api-ref-bundler'
|
|
3
|
+
import { parse } from 'yaml'
|
|
4
|
+
import type { Document } from './types.ts'
|
|
5
|
+
|
|
6
|
+
const urlRegExp = /^https?:\/+/i
|
|
7
|
+
|
|
8
|
+
async function readSource(sourcePath: string): Promise<string> {
|
|
9
|
+
if (urlRegExp.test(sourcePath)) {
|
|
10
|
+
// api-ref-bundler joins relative refs with posix normalization, collapsing `https://` to
|
|
11
|
+
// `https:/`. The WHATWG URL parser restores the double slash.
|
|
12
|
+
const url = new URL(sourcePath)
|
|
13
|
+
const response = await fetch(url)
|
|
14
|
+
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return response.text()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return read(sourcePath)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function resolveSource(sourcePath: string): Promise<object | string> {
|
|
26
|
+
const data = await readSource(sourcePath)
|
|
27
|
+
|
|
28
|
+
if (sourcePath.toLowerCase().endsWith('.md')) {
|
|
29
|
+
return data
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parse(data) as object
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
37
|
+
*
|
|
38
|
+
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
39
|
+
* pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
|
|
40
|
+
* can then emit a named type with an import instead of inlining the shape. Sources are read with
|
|
41
|
+
* the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
|
|
42
|
+
*
|
|
43
|
+
* @example Local file
|
|
44
|
+
* `const document = await bundleDocument('./openapi.yaml')`
|
|
45
|
+
*
|
|
46
|
+
* @example Remote URL
|
|
47
|
+
* `const document = await bundleDocument('https://example.com/openapi.yaml')`
|
|
48
|
+
*/
|
|
49
|
+
export async function bundleDocument(pathOrUrl: string): Promise<Document> {
|
|
50
|
+
const cache = new Map<string, Promise<object | string>>()
|
|
51
|
+
|
|
52
|
+
const resolver = (sourcePath: string) => {
|
|
53
|
+
// api-ref-bundler refers to the same URL as both `https://` and the posix-normalized
|
|
54
|
+
// `https:/`, so cache on the canonical href to fetch each source once.
|
|
55
|
+
const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath
|
|
56
|
+
const cached = cache.get(key)
|
|
57
|
+
if (cached) {
|
|
58
|
+
return cached
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = resolveSource(sourcePath)
|
|
62
|
+
cache.set(key, result)
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// api-ref-bundler swallows resolver errors and leaves refs unresolved, so surface an
|
|
67
|
+
// unreadable input document as a hard error before bundling.
|
|
68
|
+
await resolver(pathOrUrl)
|
|
69
|
+
|
|
70
|
+
return (await bundle(pathOrUrl, resolver)) as Document
|
|
71
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -31,6 +31,12 @@ export const DEFAULT_PARSER_OPTIONS = {
|
|
|
31
31
|
*/
|
|
32
32
|
export const SCHEMA_REF_PREFIX = '#/components/schemas/' as const
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* HTTP methods that count as operations on an OpenAPI path item. Other keys
|
|
36
|
+
* (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
|
|
37
|
+
*/
|
|
38
|
+
export const SUPPORTED_METHODS: ReadonlySet<string> = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'])
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* OpenAPI version string written into the stub document created during multi-spec merges.
|
|
36
42
|
*/
|
|
@@ -67,7 +73,7 @@ export const structuralKeys = new Set(['properties', 'items', 'additionalPropert
|
|
|
67
73
|
*
|
|
68
74
|
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
69
75
|
* 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
|
|
76
|
+
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname` and
|
|
71
77
|
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
72
78
|
*
|
|
73
79
|
* @example
|
|
@@ -79,6 +85,14 @@ export const structuralKeys = new Set(['properties', 'items', 'additionalPropert
|
|
|
79
85
|
* formatMap['float'] // 'number'
|
|
80
86
|
* ```
|
|
81
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
90
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
91
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
92
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
93
|
+
*/
|
|
94
|
+
export const specialCasedFormats: ReadonlySet<string> = new Set(['int64', 'date-time', 'date', 'time'])
|
|
95
|
+
|
|
82
96
|
export const formatMap = {
|
|
83
97
|
uuid: 'uuid',
|
|
84
98
|
email: 'email',
|
|
@@ -112,8 +126,8 @@ export const formatMap = {
|
|
|
112
126
|
export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
|
|
113
127
|
|
|
114
128
|
/**
|
|
115
|
-
* Maps
|
|
116
|
-
*
|
|
129
|
+
* Maps the `unknownType`/`emptySchemaType` option string to its `ScalarSchemaType` constant.
|
|
130
|
+
* A `Map` (over a plain object) lets callers test key membership with `.has()`.
|
|
117
131
|
*/
|
|
118
132
|
export const typeOptionMap = new Map<'any' | 'unknown' | 'void', ast.ScalarSchemaType>([
|
|
119
133
|
['any', ast.schemaTypes.any],
|
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
|