@kubb/adapter-oas 5.0.0-alpha.16 → 5.0.0-alpha.18
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 +796 -925
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +71 -148
- package/dist/index.js +796 -921
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/adapter.ts +62 -69
- package/src/constants.ts +56 -17
- package/src/discriminator.ts +95 -21
- package/src/factory.ts +151 -0
- package/src/guards.ts +68 -0
- package/src/parser.ts +222 -403
- package/src/refs.ts +57 -0
- package/src/resolvers.ts +490 -0
- package/src/types.ts +159 -48
- package/src/infer.ts +0 -85
- package/src/naming.ts +0 -25
- package/src/oas/Oas.ts +0 -615
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -77
- package/src/oas/utils.ts +0 -401
- package/src/refResolver.ts +0 -65
package/src/parser.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { URLPath } from '@internals/utils'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
childName,
|
|
4
|
+
createDiscriminantNode,
|
|
4
5
|
createOperation,
|
|
5
6
|
createParameter,
|
|
6
7
|
createProperty,
|
|
7
8
|
createResponse,
|
|
8
9
|
createRoot,
|
|
9
10
|
createSchema,
|
|
11
|
+
enumPropName,
|
|
10
12
|
extractRefName,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
findDiscriminator,
|
|
14
|
+
mergeAdjacentObjects,
|
|
13
15
|
narrowSchema,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
type ParserOptions,
|
|
17
|
+
setDiscriminatorEnum,
|
|
18
|
+
setEnumName,
|
|
19
|
+
simplifyUnion,
|
|
16
20
|
} from '@kubb/ast'
|
|
17
21
|
import type {
|
|
18
22
|
DistributiveOmit,
|
|
19
23
|
HttpMethod,
|
|
20
|
-
MediaType,
|
|
21
24
|
OperationNode,
|
|
22
25
|
ParameterLocation,
|
|
23
26
|
ParameterNode,
|
|
@@ -27,61 +30,44 @@ import type {
|
|
|
27
30
|
RootNode,
|
|
28
31
|
ScalarSchemaType,
|
|
29
32
|
SchemaNode,
|
|
30
|
-
SchemaType,
|
|
31
33
|
StatusCode,
|
|
32
34
|
} from '@kubb/ast/types'
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
import BaseOas from 'oas'
|
|
36
|
+
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
|
|
37
|
+
import { isDiscriminator, isNullable, isReference } from './guards.ts'
|
|
38
|
+
import { resolveRef } from './refs.ts'
|
|
39
|
+
import {
|
|
40
|
+
buildSchemaNode,
|
|
41
|
+
flattenSchema,
|
|
42
|
+
getDateType,
|
|
43
|
+
getMediaType,
|
|
44
|
+
getParameters,
|
|
45
|
+
getPrimitiveType,
|
|
46
|
+
getRequestSchema,
|
|
47
|
+
getResponseSchema,
|
|
48
|
+
getSchemas,
|
|
49
|
+
getSchemaType,
|
|
50
|
+
} from './resolvers.ts'
|
|
51
|
+
import type { contentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
|
|
42
52
|
|
|
43
53
|
/**
|
|
44
|
-
* Construction-time
|
|
54
|
+
* Construction-time context for the OAS parser.
|
|
55
|
+
*
|
|
56
|
+
* Holds the raw OpenAPI document and optional content-type override used when extracting
|
|
57
|
+
* request/response schemas.
|
|
45
58
|
*/
|
|
46
|
-
export type
|
|
59
|
+
export type OasParserContext = {
|
|
60
|
+
document: Document
|
|
47
61
|
contentType?: contentType
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
/**
|
|
51
|
-
* Looks up the Kubb `SchemaType` for a given OAS `format` string.
|
|
52
|
-
* Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
|
|
53
|
-
* which are handled separately because their output depends on parser options.
|
|
54
|
-
*/
|
|
55
|
-
function formatToSchemaType(format: string): SchemaType | undefined {
|
|
56
|
-
return formatMap[format as keyof typeof formatMap]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
61
|
-
* Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
|
|
62
|
-
* `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
|
|
63
|
-
*/
|
|
64
|
-
function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
|
|
65
|
-
if (type === 'number' || type === 'integer' || type === 'bigint') return type
|
|
66
|
-
if (type === 'boolean') return 'boolean'
|
|
67
|
-
return 'string'
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
|
|
72
|
-
* Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
|
|
73
|
-
*/
|
|
74
|
-
function toMediaType(contentType: string): MediaType | undefined {
|
|
75
|
-
return Object.values(mediaTypes).includes(contentType as MediaType) ? (contentType as MediaType) : undefined
|
|
76
|
-
}
|
|
77
|
-
|
|
78
64
|
/**
|
|
79
65
|
* Pre-computed per-schema context passed to every `convert*` branch handler.
|
|
80
66
|
* Grouping these values avoids repeating the same derivations across all branches.
|
|
81
67
|
*/
|
|
82
68
|
type SchemaContext = {
|
|
83
69
|
schema: SchemaObject
|
|
84
|
-
name: string | undefined
|
|
70
|
+
name: string | null | undefined
|
|
85
71
|
nullable: true | undefined
|
|
86
72
|
defaultValue: unknown
|
|
87
73
|
/**
|
|
@@ -93,144 +79,31 @@ type SchemaContext = {
|
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
/**
|
|
96
|
-
*
|
|
82
|
+
* Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
|
|
83
|
+
* enum values into the items sub-schema. This pattern is technically invalid OAS
|
|
84
|
+
* but appears in the wild and must be handled gracefully.
|
|
97
85
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
parse: <TOptions extends Partial<ParserOptions> = object>(options?: TOptions) => RootNode
|
|
104
|
-
convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<ParserOptions> = object>(
|
|
105
|
-
params: { schema: TSchema; name?: string },
|
|
106
|
-
options?: TOptions,
|
|
107
|
-
) => InferSchemaNode<TSchema, TOptions extends { dateType: ParserOptions['dateType'] } ? TOptions['dateType'] : 'string'>
|
|
108
|
-
/**
|
|
109
|
-
* Walks `node` and replaces each `ref` value with the name returned by
|
|
110
|
-
* `resolveName`. The callback receives the full `$ref` path (e.g. `#/components/schemas/Order`)
|
|
111
|
-
* when available, falling back to the short name. Pass a no-op (`(n) => n`) to skip resolution.
|
|
112
|
-
*
|
|
113
|
-
* The optional `resolveEnumName` callback is called for inline `enum` nodes and should return
|
|
114
|
-
* the transformed name to use (e.g. with a plugin `transformers.name` applied).
|
|
115
|
-
*/
|
|
116
|
-
resolveRefs: (node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined) => SchemaNode
|
|
86
|
+
function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
87
|
+
const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
|
|
88
|
+
const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
|
|
89
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema
|
|
117
90
|
|
|
118
|
-
|
|
119
|
-
* Map from original `$ref` paths to their collision-resolved schema names.
|
|
120
|
-
* e.g. `'#/components/schemas/Order'` → `'OrderSchema'`
|
|
121
|
-
*
|
|
122
|
-
* Pass this to the standalone `getImports()` to resolve imports without holding
|
|
123
|
-
* a reference to the full parser or OAS instance.
|
|
124
|
-
*/
|
|
125
|
-
nameMapping: Map<string, string>
|
|
91
|
+
return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
|
|
126
92
|
}
|
|
127
93
|
|
|
128
94
|
/**
|
|
129
|
-
*
|
|
130
|
-
* the `@kubb/ast` tree.
|
|
131
|
-
*
|
|
132
|
-
* Options are passed per-call to `parse` or `convertSchema` rather than
|
|
133
|
-
* at construction time, keeping the factory lightweight.
|
|
95
|
+
* Builds the internal converter functions for a given `OasParserContext`.
|
|
134
96
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* No code is generated here; the resulting tree is spec-agnostic and can
|
|
139
|
-
* be consumed by any downstream plugin (plugin-ts, plugin-zod, …).
|
|
140
|
-
*
|
|
141
|
-
* @example
|
|
142
|
-
* ```ts
|
|
143
|
-
* const parser = createOasParser(oas)
|
|
144
|
-
* const root = parser.parse({ emptySchemaType: 'unknown' })
|
|
145
|
-
* ```
|
|
97
|
+
* All `convert*` functions are defined as function declarations so they can freely
|
|
98
|
+
* reference each other and `parseSchema` via JS hoisting (mutual recursion).
|
|
146
99
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
|
|
150
|
-
const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType })
|
|
151
|
-
|
|
152
|
-
const TYPE_OPTION_MAP: Record<'any' | 'unknown' | 'void', ScalarSchemaType> = {
|
|
153
|
-
any: schemaTypes.any,
|
|
154
|
-
unknown: schemaTypes.unknown,
|
|
155
|
-
void: schemaTypes.void,
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
|
|
160
|
-
* Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
|
|
161
|
-
*/
|
|
162
|
-
function resolveTypeOption(value: 'any' | 'unknown' | 'void'): ScalarSchemaType {
|
|
163
|
-
return TYPE_OPTION_MAP[value]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
167
|
-
const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
|
|
168
|
-
const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
|
|
169
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema
|
|
170
|
-
return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Resolves the AST type and datetime modifiers for a date/time format, honoring the `dateType` option.
|
|
175
|
-
* Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
|
|
176
|
-
*/
|
|
177
|
-
function getDateType(
|
|
178
|
-
options: ParserOptions,
|
|
179
|
-
format: 'date-time' | 'date' | 'time',
|
|
180
|
-
): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | undefined {
|
|
181
|
-
if (!options.dateType) {
|
|
182
|
-
return undefined
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (format === 'date-time') {
|
|
186
|
-
if (options.dateType === 'date') {
|
|
187
|
-
return { type: 'date', representation: 'date' }
|
|
188
|
-
}
|
|
189
|
-
if (options.dateType === 'stringOffset') {
|
|
190
|
-
return { type: 'datetime', offset: true }
|
|
191
|
-
}
|
|
192
|
-
if (options.dateType === 'stringLocal') {
|
|
193
|
-
return { type: 'datetime', local: true }
|
|
194
|
-
}
|
|
195
|
-
return { type: 'datetime', offset: false }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (format === 'date') {
|
|
199
|
-
return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// time
|
|
203
|
-
return { type: 'time', representation: options.dateType === 'date' ? 'date' : 'string' }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Shared metadata fields included in every `createSchema` call.
|
|
208
|
-
* Centralizes the common properties so sub-handlers don't repeat them.
|
|
209
|
-
*/
|
|
210
|
-
function renderSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
|
|
211
|
-
return {
|
|
212
|
-
name,
|
|
213
|
-
nullable,
|
|
214
|
-
title: schema.title,
|
|
215
|
-
description: schema.description,
|
|
216
|
-
deprecated: schema.deprecated,
|
|
217
|
-
readOnly: schema.readOnly,
|
|
218
|
-
writeOnly: schema.writeOnly,
|
|
219
|
-
default: defaultValue,
|
|
220
|
-
example: schema.example,
|
|
221
|
-
} as const
|
|
222
|
-
}
|
|
100
|
+
function createSchemaParser(ctx: OasParserContext) {
|
|
101
|
+
const document = ctx.document
|
|
223
102
|
|
|
224
103
|
// Branch handlers — each converts one OAS schema pattern to a SchemaNode.
|
|
225
|
-
// They are defined as function declarations so they can reference each other
|
|
226
|
-
// and `convertSchema` freely (JS hoisting).
|
|
227
104
|
|
|
228
105
|
/**
|
|
229
|
-
* Converts a `$ref` schema
|
|
230
|
-
*
|
|
231
|
-
* In OAS 3.0 siblings of `$ref` are technically ignored by the spec, but Kubb intentionally
|
|
232
|
-
* preserves them so that annotations like `pattern`, `description`, and `nullable` are
|
|
233
|
-
* reflected in generated JSDoc and type modifiers.
|
|
106
|
+
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
234
107
|
*/
|
|
235
108
|
function convertRef({ schema, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
236
109
|
return createSchema({
|
|
@@ -249,19 +122,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
249
122
|
}
|
|
250
123
|
|
|
251
124
|
/**
|
|
252
|
-
* Converts
|
|
253
|
-
* or an `IntersectionSchemaNode` (multi-member `allOf`).
|
|
254
|
-
*
|
|
255
|
-
* Single-member `allOf` without sibling structural keys is the common OAS 3.0 pattern for
|
|
256
|
-
* annotating a `$ref` or primitive with extra constraints; it is flattened to avoid
|
|
257
|
-
* producing needless intersection wrappers.
|
|
258
|
-
*
|
|
259
|
-
* The flatten path is skipped when the outer schema carries structural keys that cannot be
|
|
260
|
-
* merged into annotation fields: `properties`, `required`, or `additionalProperties`.
|
|
261
|
-
* Those cases must become an intersection so the constraints are preserved.
|
|
262
|
-
*
|
|
263
|
-
* Circular references through discriminator parents are detected and skipped to prevent
|
|
264
|
-
* infinite recursion during code generation.
|
|
125
|
+
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
265
126
|
*/
|
|
266
127
|
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
|
|
267
128
|
if (
|
|
@@ -271,7 +132,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
271
132
|
schema.additionalProperties === undefined
|
|
272
133
|
) {
|
|
273
134
|
const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
|
|
274
|
-
const memberNode =
|
|
135
|
+
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
|
|
275
136
|
const { kind: _kind, ...memberNodeProps } = memberNode
|
|
276
137
|
const mergedNullable = nullable || memberNode.nullable || undefined
|
|
277
138
|
const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
|
|
@@ -291,14 +152,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
291
152
|
} as DistributiveOmit<SchemaNode, 'kind'>)
|
|
292
153
|
}
|
|
293
154
|
|
|
294
|
-
// When a child schema extends a discriminator parent via allOf and the parent's oneOf/anyOf
|
|
295
|
-
// references that child back, skip that allOf item to prevent a circular type reference.
|
|
296
|
-
// When an item is skipped, collect its discriminant value so it can be injected below.
|
|
297
155
|
const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
|
|
298
156
|
const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
299
157
|
.filter((item) => {
|
|
300
158
|
if (!isReference(item) || !name) return true
|
|
301
|
-
const deref =
|
|
159
|
+
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
302
160
|
if (!deref || !isDiscriminator(deref)) return true
|
|
303
161
|
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
304
162
|
if (!parentUnion) return true
|
|
@@ -306,7 +164,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
306
164
|
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
307
165
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
308
166
|
if (inOneOf || inMapping) {
|
|
309
|
-
const discriminatorValue =
|
|
167
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
|
|
310
168
|
if (discriminatorValue) {
|
|
311
169
|
filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
|
|
312
170
|
}
|
|
@@ -314,13 +172,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
314
172
|
}
|
|
315
173
|
return true
|
|
316
174
|
})
|
|
317
|
-
.map((s) =>
|
|
175
|
+
.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
|
|
318
176
|
|
|
319
|
-
// Track where allOf-derived members end so each portion can be merged independently.
|
|
320
177
|
const syntheticStart = allOfMembers.length
|
|
321
178
|
|
|
322
|
-
// When `required` lists keys not present in the outer `properties`, resolve them from
|
|
323
|
-
// the allOf member schemas and inject them as extra intersection members.
|
|
324
179
|
if (Array.isArray(schema.required) && schema.required.length) {
|
|
325
180
|
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : new Set<string>()
|
|
326
181
|
const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
|
|
@@ -328,14 +183,14 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
328
183
|
if (missingRequired.length) {
|
|
329
184
|
const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
|
|
330
185
|
if (!isReference(item)) return [item as SchemaObject]
|
|
331
|
-
const deref =
|
|
186
|
+
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
332
187
|
return deref && !isReference(deref) ? [deref] : []
|
|
333
188
|
})
|
|
334
189
|
|
|
335
190
|
for (const key of missingRequired) {
|
|
336
191
|
for (const resolved of resolvedMembers) {
|
|
337
192
|
if (resolved.properties?.[key]) {
|
|
338
|
-
allOfMembers.push(
|
|
193
|
+
allOfMembers.push(parseSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
|
|
339
194
|
break
|
|
340
195
|
}
|
|
341
196
|
}
|
|
@@ -345,104 +200,113 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
345
200
|
|
|
346
201
|
if (schema.properties) {
|
|
347
202
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
348
|
-
allOfMembers.push(
|
|
203
|
+
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
|
|
349
204
|
}
|
|
350
205
|
|
|
351
|
-
// Inject a synthetic single-property object for each discriminant value collected from
|
|
352
|
-
// filtered discriminator parents so that child schemas carry the narrowed literal type.
|
|
353
206
|
for (const { propertyName, value } of filteredDiscriminantValues) {
|
|
354
|
-
allOfMembers.push(createDiscriminantNode(propertyName, value))
|
|
207
|
+
allOfMembers.push(createDiscriminantNode({ propertyName, value }))
|
|
355
208
|
}
|
|
356
209
|
|
|
357
|
-
// Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
|
|
358
210
|
return createSchema({
|
|
359
211
|
type: 'intersection',
|
|
360
|
-
members: [...
|
|
361
|
-
...
|
|
212
|
+
members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
|
|
213
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
362
214
|
})
|
|
363
215
|
}
|
|
364
216
|
|
|
365
217
|
/**
|
|
366
218
|
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
367
|
-
*
|
|
368
|
-
* Both keywords are treated identically — their members are concatenated into a single union.
|
|
369
|
-
* When sibling `properties` are present alongside `oneOf`/`anyOf`, each union member is
|
|
370
|
-
* individually intersected with the shared properties node to match the OAS pattern of
|
|
371
|
-
* adding common fields next to a discriminated union.
|
|
372
219
|
*/
|
|
373
220
|
function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
|
|
221
|
+
function pickDiscriminatorPropertyNode(node: SchemaNode, propertyName: string): SchemaNode | null {
|
|
222
|
+
const objectNode = narrowSchema(node, 'object')
|
|
223
|
+
const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
|
|
224
|
+
|
|
225
|
+
if (!discriminatorProperty) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return createSchema({
|
|
230
|
+
type: 'object',
|
|
231
|
+
primitive: 'object',
|
|
232
|
+
properties: [discriminatorProperty],
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
374
236
|
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
375
237
|
const unionBase = {
|
|
376
|
-
...
|
|
238
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
377
239
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
378
240
|
}
|
|
379
241
|
const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
|
|
380
242
|
const sharedPropertiesNode = schema.properties
|
|
381
243
|
? (() => {
|
|
382
244
|
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
383
|
-
// Strip discriminator so convertObject won't re-apply the full mapping enum.
|
|
384
245
|
const memberBaseSchema: SchemaObject = discriminator
|
|
385
246
|
? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
|
|
386
247
|
: schemaWithoutUnion
|
|
387
|
-
|
|
388
|
-
// (e.g. StatusEnum appearing twice and getting a numeric suffix).
|
|
389
|
-
return convertSchema({ schema: memberBaseSchema, name }, rawOptions)
|
|
248
|
+
return parseSchema({ schema: memberBaseSchema, name }, rawOptions)
|
|
390
249
|
})()
|
|
391
250
|
: undefined
|
|
392
251
|
|
|
393
252
|
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
394
|
-
|
|
253
|
+
const members = unionMembers.map((s) => {
|
|
254
|
+
const ref = isReference(s) ? s.$ref : undefined
|
|
255
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
|
|
256
|
+
const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
|
|
257
|
+
|
|
258
|
+
if (!discriminatorValue || !discriminator) {
|
|
259
|
+
return memberNode
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode
|
|
263
|
+
? pickDiscriminatorPropertyNode(
|
|
264
|
+
setDiscriminatorEnum({
|
|
265
|
+
node: sharedPropertiesNode,
|
|
266
|
+
propertyName: discriminator.propertyName,
|
|
267
|
+
values: [discriminatorValue],
|
|
268
|
+
}),
|
|
269
|
+
discriminator.propertyName,
|
|
270
|
+
)
|
|
271
|
+
: undefined
|
|
272
|
+
|
|
273
|
+
return createSchema({
|
|
274
|
+
type: 'intersection',
|
|
275
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue })],
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const unionNode = createSchema({
|
|
395
280
|
type: 'union',
|
|
396
281
|
...unionBase,
|
|
397
|
-
members
|
|
398
|
-
|
|
399
|
-
const discriminatorValue = resolveDiscriminatorValue(discriminator?.mapping, ref)
|
|
400
|
-
const memberNode = convertSchema({ schema: s as SchemaObject }, rawOptions)
|
|
401
|
-
|
|
402
|
-
if (sharedPropertiesNode) {
|
|
403
|
-
const narrowedProperties =
|
|
404
|
-
discriminatorValue && discriminator
|
|
405
|
-
? applyDiscriminatorEnum({
|
|
406
|
-
node: sharedPropertiesNode,
|
|
407
|
-
propertyName: discriminator.propertyName,
|
|
408
|
-
values: [discriminatorValue],
|
|
409
|
-
})
|
|
410
|
-
: sharedPropertiesNode
|
|
411
|
-
|
|
412
|
-
return createSchema({
|
|
413
|
-
type: 'intersection',
|
|
414
|
-
members: [memberNode, narrowedProperties],
|
|
415
|
-
})
|
|
416
|
-
}
|
|
282
|
+
members,
|
|
283
|
+
})
|
|
417
284
|
|
|
418
|
-
|
|
285
|
+
if (!sharedPropertiesNode) {
|
|
286
|
+
return unionNode
|
|
287
|
+
}
|
|
419
288
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}),
|
|
289
|
+
return createSchema({
|
|
290
|
+
type: 'intersection',
|
|
291
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
292
|
+
members: [unionNode, sharedPropertiesNode],
|
|
425
293
|
})
|
|
426
294
|
}
|
|
427
295
|
|
|
428
296
|
return createSchema({
|
|
429
297
|
type: 'union',
|
|
430
298
|
...unionBase,
|
|
431
|
-
members:
|
|
299
|
+
members: simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
|
|
432
300
|
})
|
|
433
301
|
}
|
|
434
302
|
|
|
435
303
|
/**
|
|
436
|
-
* Converts an OAS 3.1 `const` schema into
|
|
437
|
-
* `const: null` maps to a null scalar; any other value becomes a one-item enum so that generators
|
|
438
|
-
* can produce a precise literal type.
|
|
304
|
+
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
439
305
|
*/
|
|
440
306
|
function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
441
307
|
const constValue = schema.const
|
|
442
308
|
|
|
443
309
|
if (constValue === null) {
|
|
444
|
-
// Do not propagate `nullable` here: the type is already `null`, so marking it
|
|
445
|
-
// nullable too would cause the printer to emit `null | null`.
|
|
446
310
|
return createSchema({
|
|
447
311
|
type: 'null',
|
|
448
312
|
primitive: 'null',
|
|
@@ -458,19 +322,17 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
458
322
|
type: 'enum',
|
|
459
323
|
primitive: constPrimitive,
|
|
460
324
|
enumValues: [constValue as string | number | boolean],
|
|
461
|
-
...
|
|
325
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
462
326
|
})
|
|
463
327
|
}
|
|
464
328
|
|
|
465
329
|
/**
|
|
466
|
-
*
|
|
467
|
-
* Returns `
|
|
468
|
-
* (i.e. `format: 'date-time'` with `dateType: false`).
|
|
330
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
331
|
+
* Returns `null` when the format should fall through to string handling (`dateType: false`).
|
|
469
332
|
*/
|
|
470
|
-
function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode |
|
|
471
|
-
const base =
|
|
333
|
+
function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | null {
|
|
334
|
+
const base = buildSchemaNode(schema, name, nullable, defaultValue)
|
|
472
335
|
|
|
473
|
-
// int64 is option-dependent so it can't live in the static formatMap.
|
|
474
336
|
if (schema.format === 'int64') {
|
|
475
337
|
return createSchema({
|
|
476
338
|
type: options.integerType === 'bigint' ? 'bigint' : 'integer',
|
|
@@ -483,10 +345,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
483
345
|
})
|
|
484
346
|
}
|
|
485
347
|
|
|
486
|
-
// date-time / date / time are option-dependent and can't live in the static formatMap.
|
|
487
348
|
if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
|
|
488
349
|
const dateType = getDateType(options, schema.format)
|
|
489
|
-
if (!dateType) return
|
|
350
|
+
if (!dateType) return null
|
|
490
351
|
|
|
491
352
|
if (dateType.type === 'datetime') {
|
|
492
353
|
return createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
|
|
@@ -494,8 +355,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
494
355
|
return createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
|
|
495
356
|
}
|
|
496
357
|
|
|
497
|
-
const specialType =
|
|
498
|
-
if (!specialType) return
|
|
358
|
+
const specialType = getSchemaType(schema.format!)
|
|
359
|
+
if (!specialType) return null
|
|
499
360
|
|
|
500
361
|
const specialPrimitive: PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
|
|
501
362
|
|
|
@@ -511,21 +372,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
511
372
|
|
|
512
373
|
/**
|
|
513
374
|
* Converts an `enum` schema into an `EnumSchemaNode`.
|
|
514
|
-
*
|
|
515
|
-
* Handles several edge cases:
|
|
516
|
-
* - `{ type: 'array', enum }` (technically invalid OAS) — the enum is normalized into `items`.
|
|
517
|
-
* - `null` in enum values (OAS 3.0 nullable enum convention) — stripped and reflected as `nullable`.
|
|
518
|
-
* - `x-enumNames` / `x-enum-varnames` vendor extensions — produce named enum variants with explicit labels.
|
|
519
|
-
* - Numeric and boolean enums require a const-map representation because most generators cannot
|
|
520
|
-
* use string-enum syntax for non-string values.
|
|
521
375
|
*/
|
|
522
376
|
function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
|
|
523
|
-
// Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
|
|
524
377
|
if (type === 'array') {
|
|
525
|
-
return
|
|
378
|
+
return parseSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
|
|
526
379
|
}
|
|
527
380
|
|
|
528
|
-
// `null` in enum values is the OAS 3.0 convention for a nullable enum.
|
|
529
381
|
const nullInEnum = schema.enum!.includes(null)
|
|
530
382
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
531
383
|
const enumNullable = nullable || nullInEnum || undefined
|
|
@@ -546,10 +398,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
546
398
|
example: schema.example,
|
|
547
399
|
}
|
|
548
400
|
|
|
549
|
-
// x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
|
|
550
401
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
551
402
|
if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
|
|
552
|
-
const
|
|
403
|
+
const enumPrimitiveType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
|
|
553
404
|
| 'number'
|
|
554
405
|
| 'boolean'
|
|
555
406
|
| 'string'
|
|
@@ -559,16 +410,15 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
559
410
|
|
|
560
411
|
return createSchema({
|
|
561
412
|
...enumBase,
|
|
562
|
-
|
|
413
|
+
primitive: enumPrimitiveType,
|
|
563
414
|
namedEnumValues: sourceValues.map((label, index) => ({
|
|
564
415
|
name: String(label),
|
|
565
416
|
value: extensionKey ? (filteredValues[index] ?? label) : label,
|
|
566
|
-
|
|
417
|
+
primitive: enumPrimitiveType,
|
|
567
418
|
})),
|
|
568
419
|
})
|
|
569
420
|
}
|
|
570
421
|
|
|
571
|
-
// Plain string enum (default path).
|
|
572
422
|
return createSchema({
|
|
573
423
|
...enumBase,
|
|
574
424
|
enumValues: [...new Set(filteredValues)],
|
|
@@ -576,18 +426,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
576
426
|
}
|
|
577
427
|
|
|
578
428
|
/**
|
|
579
|
-
* Converts an object-like schema
|
|
580
|
-
* or `patternProperties`) into an `ObjectSchemaNode`.
|
|
581
|
-
*
|
|
582
|
-
* When a `discriminator` is present, the discriminator property's schema is replaced with an
|
|
583
|
-
* enum of the mapping keys so generators can produce a precise literal-union type for it.
|
|
584
|
-
*
|
|
585
|
-
* Property optionality follows OAS semantics:
|
|
586
|
-
* - required + not nullable → `required: true`
|
|
587
|
-
* - not required + not nullable → `optional: true`
|
|
588
|
-
* - not required + nullable → `nullish: true`
|
|
429
|
+
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
589
430
|
*/
|
|
590
|
-
|
|
591
431
|
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
|
|
592
432
|
const properties: Array<PropertyNode> = schema.properties
|
|
593
433
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
@@ -595,13 +435,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
595
435
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
596
436
|
const propNullable = isNullable(resolvedPropSchema)
|
|
597
437
|
|
|
598
|
-
const
|
|
599
|
-
const propNode =
|
|
600
|
-
let schemaNode =
|
|
438
|
+
const resolvedChildName = childName(name, propName)
|
|
439
|
+
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
440
|
+
let schemaNode = setEnumName(propNode, name, propName, options.enumSuffix)
|
|
601
441
|
|
|
602
442
|
const tupleNode = narrowSchema(schemaNode, 'tuple')
|
|
603
443
|
if (tupleNode?.items) {
|
|
604
|
-
const namedItems = tupleNode.items.map((item) =>
|
|
444
|
+
const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix))
|
|
605
445
|
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
606
446
|
schemaNode = { ...tupleNode, items: namedItems }
|
|
607
447
|
}
|
|
@@ -623,11 +463,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
623
463
|
if (additionalProperties === true) {
|
|
624
464
|
additionalPropertiesNode = true
|
|
625
465
|
} else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
626
|
-
additionalPropertiesNode =
|
|
466
|
+
additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
627
467
|
} else if (additionalProperties === false) {
|
|
628
468
|
additionalPropertiesNode = undefined
|
|
629
469
|
} else if (additionalProperties) {
|
|
630
|
-
additionalPropertiesNode = createSchema({ type:
|
|
470
|
+
additionalPropertiesNode = createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
631
471
|
}
|
|
632
472
|
|
|
633
473
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
@@ -637,8 +477,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
637
477
|
Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
|
|
638
478
|
pattern,
|
|
639
479
|
patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
|
|
640
|
-
? createSchema({ type:
|
|
641
|
-
:
|
|
480
|
+
? createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
481
|
+
: parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
|
|
642
482
|
]),
|
|
643
483
|
)
|
|
644
484
|
: undefined
|
|
@@ -649,16 +489,14 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
649
489
|
properties,
|
|
650
490
|
additionalProperties: additionalPropertiesNode,
|
|
651
491
|
patternProperties,
|
|
652
|
-
...
|
|
492
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
653
493
|
})
|
|
654
494
|
|
|
655
|
-
// When a discriminator is present, replace the discriminator property's schema
|
|
656
|
-
// with an enum of the mapping keys for a precise literal-union type.
|
|
657
495
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
658
496
|
const discPropName = schema.discriminator.propertyName
|
|
659
497
|
const values = Object.keys(schema.discriminator.mapping)
|
|
660
|
-
const enumName = name ?
|
|
661
|
-
return
|
|
498
|
+
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
|
|
499
|
+
return setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
|
|
662
500
|
}
|
|
663
501
|
|
|
664
502
|
return objectNode
|
|
@@ -666,15 +504,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
666
504
|
|
|
667
505
|
/**
|
|
668
506
|
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
669
|
-
*
|
|
670
|
-
* Each `prefixItems` element maps to a positional tuple slot. When an explicit `items` schema
|
|
671
|
-
* is present alongside `prefixItems`, it becomes the rest element. When `items` is absent,
|
|
672
|
-
* a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
|
|
673
|
-
* means additional items are allowed).
|
|
674
507
|
*/
|
|
675
508
|
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
|
|
676
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) =>
|
|
677
|
-
const rest = schema.items ?
|
|
509
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
|
|
510
|
+
const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
|
|
678
511
|
|
|
679
512
|
return createSchema({
|
|
680
513
|
type: 'tuple',
|
|
@@ -683,22 +516,17 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
683
516
|
rest,
|
|
684
517
|
min: schema.minItems,
|
|
685
518
|
max: schema.maxItems,
|
|
686
|
-
...
|
|
519
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
687
520
|
})
|
|
688
521
|
}
|
|
689
522
|
|
|
690
523
|
/**
|
|
691
524
|
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
692
|
-
*
|
|
693
|
-
* When the items schema is an inline enum, a name derived from the parent array's name and
|
|
694
|
-
* `enumSuffix` is forwarded so generators can emit a named enum declaration.
|
|
695
525
|
*/
|
|
696
526
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
|
|
697
527
|
const rawItems = schema.items as SchemaObject | undefined
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, options.enumSuffix) : undefined
|
|
701
|
-
const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
528
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(undefined, name, options.enumSuffix) : undefined
|
|
529
|
+
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
702
530
|
|
|
703
531
|
return createSchema({
|
|
704
532
|
type: 'array',
|
|
@@ -707,12 +535,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
707
535
|
min: schema.minItems,
|
|
708
536
|
max: schema.maxItems,
|
|
709
537
|
unique: schema.uniqueItems ?? undefined,
|
|
710
|
-
...
|
|
538
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
711
539
|
})
|
|
712
540
|
}
|
|
713
541
|
|
|
714
542
|
/**
|
|
715
|
-
* Converts a `type: 'string'` schema
|
|
543
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
716
544
|
*/
|
|
717
545
|
function convertString({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
718
546
|
return createSchema({
|
|
@@ -721,12 +549,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
721
549
|
min: schema.minLength,
|
|
722
550
|
max: schema.maxLength,
|
|
723
551
|
pattern: schema.pattern,
|
|
724
|
-
...
|
|
552
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
725
553
|
})
|
|
726
554
|
}
|
|
727
555
|
|
|
728
556
|
/**
|
|
729
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema
|
|
557
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
730
558
|
*/
|
|
731
559
|
function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): SchemaNode {
|
|
732
560
|
return createSchema({
|
|
@@ -736,23 +564,23 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
736
564
|
max: schema.maximum,
|
|
737
565
|
exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
|
|
738
566
|
exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
|
|
739
|
-
...
|
|
567
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
740
568
|
})
|
|
741
569
|
}
|
|
742
570
|
|
|
743
571
|
/**
|
|
744
|
-
* Converts a `type: 'boolean'` schema
|
|
572
|
+
* Converts a `type: 'boolean'` schema.
|
|
745
573
|
*/
|
|
746
574
|
function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
747
575
|
return createSchema({
|
|
748
576
|
type: 'boolean',
|
|
749
577
|
primitive: 'boolean',
|
|
750
|
-
...
|
|
578
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
751
579
|
})
|
|
752
580
|
}
|
|
753
581
|
|
|
754
582
|
/**
|
|
755
|
-
* Converts an explicit `type: 'null'`
|
|
583
|
+
* Converts an explicit `type: 'null'` schema.
|
|
756
584
|
*/
|
|
757
585
|
function convertNull({ schema, name, nullable }: SchemaContext): SchemaNode {
|
|
758
586
|
return createSchema({
|
|
@@ -767,67 +595,42 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
767
595
|
}
|
|
768
596
|
|
|
769
597
|
/**
|
|
770
|
-
* Central dispatcher
|
|
598
|
+
* Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
771
599
|
*
|
|
772
|
-
* Dispatch order (first match wins):
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* 3. `oneOf` / `anyOf` union
|
|
776
|
-
* 4. `const` literal (OAS 3.1)
|
|
777
|
-
* 5. `format`-based special type (date/time, uuid, blob, …)
|
|
778
|
-
* 6. OAS 3.1 `contentMediaType: 'application/octet-stream'` blob
|
|
779
|
-
* 7. OAS 3.1 multi-type array → union or fallthrough
|
|
780
|
-
* 8. Constraint-inferred type (minLength/maxLength → string; minimum/maximum → number)
|
|
781
|
-
* 9. `enum` values
|
|
782
|
-
* 10. Object / array / tuple / scalar by `type`
|
|
783
|
-
* 11. Empty schema fallback (`emptySchemaType` option)
|
|
600
|
+
* Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
|
|
601
|
+
* → octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
|
|
602
|
+
* → empty-schema fallback (`emptySchemaType` option).
|
|
784
603
|
*/
|
|
785
|
-
function
|
|
604
|
+
function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ParserOptions>): SchemaNode {
|
|
786
605
|
const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
|
|
787
|
-
// Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
|
|
788
|
-
// schema before parsing, so simple annotation patterns don't produce needless intersections.
|
|
789
606
|
const flattenedSchema = flattenSchema(schema)
|
|
790
607
|
if (flattenedSchema && flattenedSchema !== schema) {
|
|
791
|
-
return
|
|
608
|
+
return parseSchema({ schema: flattenedSchema, name }, rawOptions)
|
|
792
609
|
}
|
|
793
610
|
|
|
794
611
|
const nullable = isNullable(schema) || undefined
|
|
795
612
|
const defaultValue = schema.default === null && nullable ? undefined : schema.default
|
|
796
|
-
// Normalize OAS 3.1 multi-type array to a single type string for the dispatch below.
|
|
797
613
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
798
614
|
|
|
799
615
|
const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
|
|
800
616
|
|
|
801
|
-
// $ref — pointer to another definition.
|
|
802
|
-
// In OAS 3.0 siblings of $ref are technically ignored, but Kubb intentionally preserves them
|
|
803
|
-
// so that annotations like `pattern`, `description`, and `nullable` are reflected in generated code.
|
|
804
617
|
if (isReference(schema)) return convertRef(ctx)
|
|
805
618
|
|
|
806
|
-
// Composition keywords
|
|
807
619
|
if (schema.allOf?.length) return convertAllOf(ctx)
|
|
808
620
|
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
809
621
|
if (unionMembers.length) return convertUnion(ctx)
|
|
810
622
|
|
|
811
|
-
// OAS 3.1 const — a single fixed value, semantically equivalent to a one-item enum.
|
|
812
|
-
// `const: undefined` falls through to the empty-type fallback.
|
|
813
623
|
if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
|
|
814
624
|
|
|
815
|
-
// Format-based special types take precedence over `type`.
|
|
816
|
-
// `convertFormat` returns undefined when format should fall through to string (dateType: false).
|
|
817
|
-
// see https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
|
818
625
|
if (schema.format) {
|
|
819
626
|
const formatResult = convertFormat(ctx)
|
|
820
627
|
if (formatResult) return formatResult
|
|
821
628
|
}
|
|
822
629
|
|
|
823
|
-
// OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
|
|
824
630
|
if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
|
|
825
|
-
return createSchema({ type: 'blob', primitive: 'string', ...
|
|
631
|
+
return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaNode(schema, name, nullable, defaultValue) })
|
|
826
632
|
}
|
|
827
633
|
|
|
828
|
-
// OAS 3.1: `type` may be an array — e.g. `["string", "integer", "null"]`.
|
|
829
|
-
// `null` in the array is the 3.1 equivalent of `nullable: true`; strip it and set the flag.
|
|
830
|
-
// When 2+ non-null types remain, produce a union; when exactly 1 non-null type remains, fall through.
|
|
831
634
|
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
832
635
|
const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
|
|
833
636
|
const arrayNullable = schema.type.includes('null') || nullable || undefined
|
|
@@ -835,15 +638,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
835
638
|
if (nonNullTypes.length > 1) {
|
|
836
639
|
return createSchema({
|
|
837
640
|
type: 'union',
|
|
838
|
-
members: nonNullTypes.map((t) =>
|
|
839
|
-
...
|
|
641
|
+
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
642
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
840
643
|
})
|
|
841
644
|
}
|
|
842
645
|
}
|
|
843
646
|
|
|
844
|
-
// Infer type from constraints when no explicit type is provided.
|
|
845
|
-
// minLength / maxLength / pattern → string; minimum / maximum → number.
|
|
846
|
-
// Note: minItems/maxItems do NOT infer array — arrays require an `items` key.
|
|
847
647
|
if (!type) {
|
|
848
648
|
if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
|
|
849
649
|
return convertString(ctx)
|
|
@@ -863,20 +663,19 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
863
663
|
if (type === 'boolean') return convertBoolean(ctx)
|
|
864
664
|
if (type === 'null') return convertNull(ctx)
|
|
865
665
|
|
|
866
|
-
const emptyType =
|
|
666
|
+
const emptyType = typeOptionMap.get(options.emptySchemaType)!
|
|
867
667
|
return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
|
|
868
668
|
}
|
|
869
669
|
|
|
870
670
|
/**
|
|
871
|
-
* Converts a
|
|
872
|
-
* When the parameter has no `schema`, falls back to `unknownType`; `$ref` schemas are resolved through `convertSchema` to produce a proper named type reference.
|
|
671
|
+
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
873
672
|
*/
|
|
874
673
|
function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
|
|
875
674
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
876
675
|
|
|
877
676
|
const schema: SchemaNode = param['schema']
|
|
878
|
-
?
|
|
879
|
-
: createSchema({ type:
|
|
677
|
+
? parseSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
678
|
+
: createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
880
679
|
|
|
881
680
|
return createParameter({
|
|
882
681
|
name: param['name'] as string,
|
|
@@ -890,14 +689,15 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
890
689
|
}
|
|
891
690
|
|
|
892
691
|
/**
|
|
893
|
-
* Converts an OAS `Operation` into an `OperationNode
|
|
894
|
-
* request body, and all response codes into their AST node equivalents.
|
|
692
|
+
* Converts an OAS `Operation` into an `OperationNode`.
|
|
895
693
|
*/
|
|
896
|
-
function parseOperation(options: ParserOptions,
|
|
897
|
-
const parameters: Array<ParameterNode> =
|
|
694
|
+
function parseOperation(options: ParserOptions, operation: Operation): OperationNode {
|
|
695
|
+
const parameters: Array<ParameterNode> = getParameters(document, operation).map((param) =>
|
|
696
|
+
parseParameter(options, param as unknown as Record<string, unknown>),
|
|
697
|
+
)
|
|
898
698
|
|
|
899
|
-
const requestBodySchema =
|
|
900
|
-
const requestBodySchemaNode = requestBodySchema ?
|
|
699
|
+
const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
|
|
700
|
+
const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
|
|
901
701
|
|
|
902
702
|
const requestBodyDescription =
|
|
903
703
|
operation.schema.requestBody && !isReference(operation.schema.requestBody)
|
|
@@ -920,12 +720,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
920
720
|
|
|
921
721
|
const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
|
|
922
722
|
const responseObj = operation.getResponseByStatusCode(statusCode)
|
|
923
|
-
const responseSchema =
|
|
723
|
+
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
|
|
924
724
|
|
|
925
725
|
const schema =
|
|
926
726
|
responseSchema && Object.keys(responseSchema).length > 0
|
|
927
|
-
?
|
|
928
|
-
: createSchema({ type:
|
|
727
|
+
? parseSchema({ schema: responseSchema }, options)
|
|
728
|
+
: createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
929
729
|
|
|
930
730
|
const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
|
|
931
731
|
|
|
@@ -934,7 +734,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
934
734
|
? (responseObj as { content?: Record<string, unknown> }).content
|
|
935
735
|
: undefined
|
|
936
736
|
|
|
937
|
-
const mediaType = rawContent ?
|
|
737
|
+
const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
|
|
938
738
|
|
|
939
739
|
const keysToOmit = responseSchema?.properties
|
|
940
740
|
? Object.entries(responseSchema.properties)
|
|
@@ -965,38 +765,57 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
|
|
|
965
765
|
})
|
|
966
766
|
}
|
|
967
767
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
* a `RootNode` — the top-level node of the `@kubb/ast` tree.
|
|
971
|
-
*/
|
|
972
|
-
function parse<TOptions extends Partial<ParserOptions> = object>(options?: TOptions): RootNode {
|
|
973
|
-
const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
|
|
768
|
+
return { parseSchema, parseOperation, parseParameter }
|
|
769
|
+
}
|
|
974
770
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
771
|
+
/**
|
|
772
|
+
* Converts a single `SchemaObject` into a `SchemaNode`.
|
|
773
|
+
*
|
|
774
|
+
* @example
|
|
775
|
+
* ```ts
|
|
776
|
+
* const ctx = { document }
|
|
777
|
+
* parseSchema(ctx, { schema: { type: 'string', format: 'uuid' } })
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
export function parseSchema(ctx: OasParserContext, { schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
|
|
781
|
+
return createSchemaParser(ctx).parseSchema({ schema, name }, options)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Converts the entire OpenAPI spec into a `RootNode` (the top-level `@kubb/ast` tree).
|
|
786
|
+
*
|
|
787
|
+
* This is the main entry point: `OpenAPI / Swagger → Kubb AST`.
|
|
788
|
+
* No code is generated here — the resulting tree is spec-agnostic and consumed by
|
|
789
|
+
* downstream plugins (`plugin-ts`, `plugin-zod`, …).
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* ```ts
|
|
793
|
+
* const document = await parseFromConfig(config)
|
|
794
|
+
* const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
|
|
795
|
+
* ```
|
|
796
|
+
*/
|
|
797
|
+
export function parseOas(
|
|
798
|
+
document: Document,
|
|
799
|
+
options: Partial<ParserOptions> & { contentType?: contentType } = {},
|
|
800
|
+
): { root: RootNode; nameMapping: Map<string, string> } {
|
|
801
|
+
const { contentType, ...parserOptions } = options
|
|
802
|
+
const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...parserOptions }
|
|
978
803
|
|
|
979
|
-
|
|
804
|
+
const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType })
|
|
805
|
+
const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
|
|
980
806
|
|
|
981
|
-
|
|
982
|
-
Object.entries(methods)
|
|
983
|
-
.map(([, operation]) => (operation ? parseOperation(mergedOptions, oas, operation) : null))
|
|
984
|
-
.filter((op): op is OperationNode => op !== null),
|
|
985
|
-
)
|
|
807
|
+
const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
|
|
986
808
|
|
|
987
|
-
|
|
988
|
-
|
|
809
|
+
const baseOas = new BaseOas(document)
|
|
810
|
+
const paths = baseOas.getPaths()
|
|
811
|
+
|
|
812
|
+
const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
|
|
813
|
+
Object.entries(methods)
|
|
814
|
+
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
815
|
+
.filter((op): op is OperationNode => op !== null),
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
const root = createRoot({ schemas, operations })
|
|
989
819
|
|
|
990
|
-
|
|
991
|
-
node: SchemaNode,
|
|
992
|
-
resolveName: (ref: string) => string | undefined,
|
|
993
|
-
resolveEnumName?: (name: string) => string | undefined,
|
|
994
|
-
): SchemaNode => resolveRefsNode({ node, nameMapping, resolveName, resolveEnumName })
|
|
995
|
-
|
|
996
|
-
return {
|
|
997
|
-
parse,
|
|
998
|
-
convertSchema,
|
|
999
|
-
resolveRefs,
|
|
1000
|
-
nameMapping,
|
|
1001
|
-
} as OasParser
|
|
820
|
+
return { root, nameMapping }
|
|
1002
821
|
}
|