@kubb/adapter-oas 5.0.0-beta.4 → 5.0.0-beta.40
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 +100 -0
- package/dist/index.cjs +900 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +52 -69
- package/dist/index.js +902 -310
- package/dist/index.js.map +1 -1
- package/extension.yaml +144 -57
- package/package.json +6 -4
- package/src/adapter.bench.ts +64 -0
- package/src/adapter.ts +143 -48
- package/src/constants.ts +8 -0
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +31 -47
- package/src/factory.ts +38 -12
- package/src/index.ts +2 -2
- package/src/parser.ts +233 -137
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +77 -38
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +278 -0
- package/src/types.ts +34 -11
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/adapter.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { ast, createAdapter } from '@kubb/core'
|
|
2
|
+
import type { AdapterSource } from '@kubb/core'
|
|
3
|
+
import BaseOas from 'oas'
|
|
2
4
|
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { assertInputExists, parseDocument, parseFromConfig, validateDocument } from './factory.ts'
|
|
6
|
+
import { createSchemaParser } from './parser.ts'
|
|
7
|
+
import { getSchemas } from './resolvers.ts'
|
|
8
|
+
import { createInputStream, preScan, resolveBaseUrl } from './stream.ts'
|
|
7
9
|
import type { AdapterOas, Document } from './types.ts'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
*
|
|
12
|
+
* Canonical adapter name for `@kubb/adapter-oas`. Used for driver lookups.
|
|
11
13
|
*/
|
|
12
14
|
export const adapterOasName = 'oas' satisfies AdapterOas['name']
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
|
|
18
|
+
* file at `input.path`, validates it, resolves the base URL, and converts every
|
|
19
|
+
* schema and operation into the universal AST that every downstream plugin
|
|
20
|
+
* consumes.
|
|
16
21
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* Configure once on `defineConfig`. The adapter's choices (date representation,
|
|
23
|
+
* integer width, server URL) apply to every plugin in the build.
|
|
19
24
|
*
|
|
20
25
|
* @example
|
|
21
26
|
* ```ts
|
|
@@ -24,8 +29,13 @@ export const adapterOasName = 'oas' satisfies AdapterOas['name']
|
|
|
24
29
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
25
30
|
*
|
|
26
31
|
* export default defineConfig({
|
|
27
|
-
*
|
|
28
|
-
*
|
|
32
|
+
* input: { path: './petStore.yaml' },
|
|
33
|
+
* output: { path: './src/gen' },
|
|
34
|
+
* adapter: adapterOas({
|
|
35
|
+
* serverIndex: 0,
|
|
36
|
+
* discriminator: 'inherit',
|
|
37
|
+
* dateType: 'date',
|
|
38
|
+
* }),
|
|
29
39
|
* plugins: [pluginTs()],
|
|
30
40
|
* })
|
|
31
41
|
* ```
|
|
@@ -37,6 +47,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
37
47
|
serverIndex,
|
|
38
48
|
serverVariables,
|
|
39
49
|
discriminator = 'strict',
|
|
50
|
+
dedupe = true,
|
|
40
51
|
dateType = DEFAULT_PARSER_OPTIONS.dateType,
|
|
41
52
|
integerType = DEFAULT_PARSER_OPTIONS.integerType,
|
|
42
53
|
unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
|
|
@@ -44,10 +55,115 @@ 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
|
+
// Cache per source and per document so one adapter instance reused across a `defineConfig` array
|
|
71
|
+
// parses each config's spec instead of replaying the first one. Keying the document by its source
|
|
72
|
+
// object still collapses a config's concurrent `stream()` (build) and `parse()` (studio) calls,
|
|
73
|
+
// which share one source object, onto a single parse. The document-derived caches key off the
|
|
74
|
+
// resulting document, so distinct configs (distinct documents) stay isolated.
|
|
75
|
+
const documentCache = new WeakMap<AdapterSource, Promise<Document>>()
|
|
76
|
+
const schemasCache = new WeakMap<Document, Promise<ReturnType<typeof getSchemas>['schemas']>>()
|
|
77
|
+
const baseOasCache = new WeakMap<Document, BaseOas>()
|
|
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 ensureBaseOas(document: Document): BaseOas {
|
|
109
|
+
const cached = baseOasCache.get(document)
|
|
110
|
+
if (cached) return cached
|
|
111
|
+
|
|
112
|
+
const baseOas = new BaseOas(document)
|
|
113
|
+
baseOasCache.set(document, baseOas)
|
|
114
|
+
return baseOas
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureSchemaParser(document: Document): ReturnType<typeof createSchemaParser> {
|
|
118
|
+
const cached = schemaParserCache.get(document)
|
|
119
|
+
if (cached) return cached
|
|
120
|
+
|
|
121
|
+
const parser = createSchemaParser({ document, contentType })
|
|
122
|
+
schemaParserCache.set(document, parser)
|
|
123
|
+
return parser
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function ensurePreScan(
|
|
127
|
+
document: Document,
|
|
128
|
+
schemas: ReturnType<typeof getSchemas>['schemas'],
|
|
129
|
+
parseSchema: ReturnType<typeof ensureSchemaParser>['parseSchema'],
|
|
130
|
+
parseOperation: ReturnType<typeof ensureSchemaParser>['parseOperation'],
|
|
131
|
+
baseOas: BaseOas,
|
|
132
|
+
): ReturnType<typeof preScan> {
|
|
133
|
+
const cached = preScanCache.get(document)
|
|
134
|
+
if (cached) return cached
|
|
135
|
+
|
|
136
|
+
const result = preScan({ schemas, parseSchema, parseOperation, baseOas, parserOptions, discriminator, dedupe })
|
|
137
|
+
preScanCache.set(document, result)
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function createStream(source: AdapterSource): Promise<ast.InputStreamNode> {
|
|
142
|
+
const document = await ensureDocument(source)
|
|
143
|
+
const schemas = await ensureSchemas(document)
|
|
144
|
+
const { parseSchema, parseOperation } = ensureSchemaParser(document)
|
|
145
|
+
const baseOas = ensureBaseOas(document)
|
|
146
|
+
const { refAliasMap, enumNames, circularNames, discriminatorChildMap, dedupePlan } = ensurePreScan(document, schemas, parseSchema, parseOperation, baseOas)
|
|
147
|
+
|
|
148
|
+
return createInputStream({
|
|
149
|
+
schemas,
|
|
150
|
+
parseSchema,
|
|
151
|
+
parseOperation,
|
|
152
|
+
baseOas,
|
|
153
|
+
parserOptions,
|
|
154
|
+
refAliasMap,
|
|
155
|
+
discriminatorChildMap,
|
|
156
|
+
dedupePlan,
|
|
157
|
+
meta: {
|
|
158
|
+
title: document.info?.title,
|
|
159
|
+
description: document.info?.description,
|
|
160
|
+
version: document.info?.version,
|
|
161
|
+
baseURL: resolveBaseUrl({ document, serverIndex, serverVariables }),
|
|
162
|
+
circularNames,
|
|
163
|
+
enumNames,
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
}
|
|
51
167
|
|
|
52
168
|
return {
|
|
53
169
|
name: adapterOasName,
|
|
@@ -58,6 +174,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
58
174
|
serverIndex,
|
|
59
175
|
serverVariables,
|
|
60
176
|
discriminator,
|
|
177
|
+
dedupe,
|
|
61
178
|
dateType,
|
|
62
179
|
integerType,
|
|
63
180
|
unknownType,
|
|
@@ -69,8 +186,10 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
69
186
|
get document() {
|
|
70
187
|
return parsedDocument
|
|
71
188
|
},
|
|
72
|
-
|
|
73
|
-
|
|
189
|
+
async validate(input, options) {
|
|
190
|
+
await assertInputExists(input)
|
|
191
|
+
const document = await parseDocument(input)
|
|
192
|
+
await validateDocument(document, options)
|
|
74
193
|
},
|
|
75
194
|
getImports(node, resolve) {
|
|
76
195
|
return ast.collectImports({
|
|
@@ -78,49 +197,25 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
78
197
|
nameMapping,
|
|
79
198
|
resolve: (schemaName) => {
|
|
80
199
|
const result = resolve(schemaName)
|
|
81
|
-
if (!result) return
|
|
200
|
+
if (!result) return null
|
|
82
201
|
|
|
83
202
|
return ast.createImport({ name: [result.name], path: result.path })
|
|
84
203
|
},
|
|
85
204
|
})
|
|
86
205
|
},
|
|
87
206
|
async parse(source) {
|
|
88
|
-
const
|
|
207
|
+
const streamNode = await createStream(source)
|
|
89
208
|
|
|
90
|
-
|
|
91
|
-
|
|
209
|
+
const collect = async <T>(iter: AsyncIterable<T>): Promise<Array<T>> => {
|
|
210
|
+
const out: Array<T> = []
|
|
211
|
+
for await (const item of iter) out.push(item)
|
|
212
|
+
return out
|
|
92
213
|
}
|
|
93
214
|
|
|
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
|
-
})
|
|
215
|
+
const [schemas, operations] = await Promise.all([collect(streamNode.schemas), collect(streamNode.operations)])
|
|
122
216
|
|
|
123
|
-
return
|
|
217
|
+
return ast.createInput({ schemas, operations, meta: streamNode.meta })
|
|
124
218
|
},
|
|
219
|
+
stream: createStream,
|
|
125
220
|
}
|
|
126
221
|
})
|
package/src/constants.ts
CHANGED
|
@@ -79,6 +79,14 @@ export const structuralKeys = new Set(['properties', 'items', 'additionalPropert
|
|
|
79
79
|
* formatMap['float'] // 'number'
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
+
/**
|
|
83
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
84
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
85
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
86
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
87
|
+
*/
|
|
88
|
+
export const specialCasedFormats: ReadonlySet<string> = new Set(['int64', 'date-time', 'date', 'time'])
|
|
89
|
+
|
|
82
90
|
export const formatMap = {
|
|
83
91
|
uuid: 'uuid',
|
|
84
92
|
email: 'email',
|
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,22 @@
|
|
|
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
|
+
* 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`.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
* ```
|
|
13
|
+
* The streaming path calls this on a small pre-parsed subset of schemas (only the
|
|
14
|
+
* discriminator parents) rather than on all schemas at once.
|
|
22
15
|
*/
|
|
23
|
-
export function
|
|
16
|
+
export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
|
|
24
17
|
const childMap = new Map<string, DiscriminatorTarget>()
|
|
25
18
|
|
|
26
|
-
for (const schema of
|
|
19
|
+
for (const schema of schemas) {
|
|
27
20
|
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
28
21
|
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
29
22
|
let unionNode = ast.narrowSchema(schema, 'union')
|
|
@@ -50,8 +43,8 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
50
43
|
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
51
44
|
if (!intersectionNode?.members) continue
|
|
52
45
|
|
|
53
|
-
let refNode:
|
|
54
|
-
let objNode:
|
|
46
|
+
let refNode: SchemaNodeByType['ref'] | null = null
|
|
47
|
+
let objNode: SchemaNodeByType['object'] | null = null
|
|
55
48
|
|
|
56
49
|
for (const m of intersectionNode.members) {
|
|
57
50
|
refNode ??= ast.narrowSchema(m, 'ref')
|
|
@@ -61,48 +54,39 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
61
54
|
if (!refNode?.name || !objNode) continue
|
|
62
55
|
|
|
63
56
|
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
64
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') :
|
|
57
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
65
58
|
if (!enumNode?.enumValues?.length) continue
|
|
66
59
|
|
|
67
60
|
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
68
61
|
if (!enumValues.length) continue
|
|
69
62
|
|
|
70
63
|
const existing = childMap.get(refNode.name)
|
|
71
|
-
if (existing) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
childMap.set(refNode.name, {
|
|
75
|
-
propertyName: discriminatorPropertyName,
|
|
76
|
-
enumValues: [...enumValues],
|
|
77
|
-
})
|
|
64
|
+
if (!existing) {
|
|
65
|
+
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
66
|
+
continue
|
|
78
67
|
}
|
|
68
|
+
existing.enumValues.push(...enumValues)
|
|
79
69
|
}
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
|
|
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
|
|
72
|
+
return childMap
|
|
73
|
+
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
77
|
+
* the discriminant property). Used by the streaming path to apply patches inline per yield
|
|
78
|
+
* without buffering all schemas.
|
|
79
|
+
*/
|
|
80
|
+
export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
|
|
81
|
+
const objectNode = ast.narrowSchema(node, 'object')
|
|
82
|
+
if (!objectNode) return node
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
name: propertyName,
|
|
98
|
-
required: true,
|
|
99
|
-
schema: enumSchema,
|
|
100
|
-
})
|
|
84
|
+
const { propertyName, enumValues } = entry
|
|
85
|
+
const enumSchema = ast.createSchema({ type: 'enum', enumValues })
|
|
86
|
+
const newProp = ast.createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
89
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
})
|
|
91
|
+
return { ...objectNode, properties: newProperties }
|
|
108
92
|
}
|
package/src/factory.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { mergeDeep, URLPath } from '@internals/utils'
|
|
2
|
+
import { exists, mergeDeep, URLPath } from '@internals/utils'
|
|
3
|
+
import { Diagnostics } from '@kubb/core'
|
|
3
4
|
import type { AdapterSource } from '@kubb/core'
|
|
4
5
|
import { bundle, loadConfig } from '@redocly/openapi-core'
|
|
5
6
|
import OASNormalize from 'oas-normalize'
|
|
@@ -74,13 +75,16 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
|
|
|
74
75
|
* ```
|
|
75
76
|
*/
|
|
76
77
|
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
|
-
}
|
|
78
|
+
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
|
|
81
79
|
|
|
82
80
|
if (documents.length === 0) {
|
|
83
|
-
throw new Error(
|
|
81
|
+
throw new Diagnostics.Error({
|
|
82
|
+
code: Diagnostics.code.inputRequired,
|
|
83
|
+
severity: 'error',
|
|
84
|
+
message: 'No OAS documents were provided for merging.',
|
|
85
|
+
help: 'Pass at least one path or document to `input.path`.',
|
|
86
|
+
location: { kind: 'config' },
|
|
87
|
+
})
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
const seed: Document = {
|
|
@@ -102,9 +106,9 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
102
106
|
* Creates a `Document` from an `AdapterSource`.
|
|
103
107
|
*
|
|
104
108
|
* Handles all three source types:
|
|
105
|
-
* - `{ type: 'path' }`
|
|
106
|
-
* - `{ type: 'paths' }`
|
|
107
|
-
* - `{ type: 'data' }`
|
|
109
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
110
|
+
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
111
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
108
112
|
*
|
|
109
113
|
* @example
|
|
110
114
|
* ```ts
|
|
@@ -112,7 +116,7 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
112
116
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
113
117
|
* ```
|
|
114
118
|
*/
|
|
115
|
-
export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
119
|
+
export async function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
116
120
|
if (source.type === 'data') {
|
|
117
121
|
if (typeof source.data === 'object') {
|
|
118
122
|
return parseDocument(structuredClone(source.data) as Document)
|
|
@@ -130,7 +134,29 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
|
130
134
|
return parseDocument(source.path)
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
const resolved = path.resolve(path.dirname(source.path), source.path)
|
|
138
|
+
await assertInputExists(resolved)
|
|
139
|
+
return parseDocument(resolved)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
144
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
145
|
+
* its parse error instead.
|
|
146
|
+
*/
|
|
147
|
+
export async function assertInputExists(input: string): Promise<void> {
|
|
148
|
+
if (new URLPath(input).isURL) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (!(await exists(input))) {
|
|
152
|
+
throw new Diagnostics.Error({
|
|
153
|
+
code: Diagnostics.code.inputNotFound,
|
|
154
|
+
severity: 'error',
|
|
155
|
+
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
156
|
+
help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
|
|
157
|
+
location: { kind: 'config' },
|
|
158
|
+
})
|
|
159
|
+
}
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
/**
|
|
@@ -160,6 +186,6 @@ export async function validateDocument(document: Document, { throwOnError = fals
|
|
|
160
186
|
throw error
|
|
161
187
|
}
|
|
162
188
|
|
|
163
|
-
// Validation failures are non-fatal
|
|
189
|
+
// Validation failures are non-fatal, mirror plugin-oas behavior
|
|
164
190
|
}
|
|
165
191
|
}
|
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,
|