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