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