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