@kubb/adapter-oas 5.0.0-alpha.4 → 5.0.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1082 -1216
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +203 -133
- package/dist/index.js +1072 -1211
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/adapter.ts +81 -76
- package/src/constants.ts +66 -66
- package/src/discriminator.ts +98 -0
- package/src/factory.ts +162 -0
- package/src/guards.ts +68 -0
- package/src/index.ts +17 -0
- package/src/parser.ts +402 -645
- package/src/refs.ts +57 -0
- package/src/resolvers.ts +489 -0
- package/src/types.ts +174 -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,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
RootNode,
|
|
21
|
-
ScalarSchemaNode,
|
|
22
|
-
ScalarSchemaType,
|
|
23
|
-
SchemaNode,
|
|
24
|
-
SchemaType,
|
|
25
|
-
StatusCode,
|
|
26
|
-
StringSchemaNode,
|
|
27
|
-
TimeSchemaNode,
|
|
28
|
-
UnionSchemaNode,
|
|
29
|
-
} from '@kubb/ast/types'
|
|
30
|
-
import { enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
|
|
31
|
-
import type { Oas } from './oas/Oas.ts'
|
|
32
|
-
import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
|
|
33
|
-
import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
|
|
34
|
-
import { applyDiscriminatorEnum, extractRefName, mergeAdjacentAnonymousObjects, simplifyUnionMembers } from './utils.ts'
|
|
1
|
+
import { URLPath } from '@internals/utils'
|
|
2
|
+
import { ast } from '@kubb/core'
|
|
3
|
+
import BaseOas from 'oas'
|
|
4
|
+
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
|
|
5
|
+
import { isDiscriminator, isNullable, isReference } from './guards.ts'
|
|
6
|
+
import { resolveRef } from './refs.ts'
|
|
7
|
+
import {
|
|
8
|
+
buildSchemaNode,
|
|
9
|
+
flattenSchema,
|
|
10
|
+
getDateType,
|
|
11
|
+
getMediaType,
|
|
12
|
+
getParameters,
|
|
13
|
+
getPrimitiveType,
|
|
14
|
+
getRequestSchema,
|
|
15
|
+
getResponseSchema,
|
|
16
|
+
getSchemas,
|
|
17
|
+
getSchemaType,
|
|
18
|
+
} from './resolvers.ts'
|
|
19
|
+
import type { ContentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
|
|
35
20
|
|
|
36
21
|
/**
|
|
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`.
|
|
22
|
+
* Construction-time context for the OAS parser.
|
|
23
|
+
*
|
|
24
|
+
* Holds the raw OpenAPI document and optional content-type override used when extracting
|
|
25
|
+
* request/response schemas.
|
|
184
26
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
27
|
+
export type OasParserContext = {
|
|
28
|
+
document: Document
|
|
29
|
+
contentType?: ContentType
|
|
187
30
|
}
|
|
188
31
|
|
|
189
32
|
/**
|
|
@@ -192,178 +35,84 @@ function toMediaType(contentType: string): MediaType | undefined {
|
|
|
192
35
|
*/
|
|
193
36
|
type SchemaContext = {
|
|
194
37
|
schema: SchemaObject
|
|
195
|
-
name: string | undefined
|
|
38
|
+
name: string | null | undefined
|
|
196
39
|
nullable: true | undefined
|
|
197
40
|
defaultValue: unknown
|
|
198
41
|
/**
|
|
199
42
|
* Normalized single type string (first element when OAS 3.1 multi-type array).
|
|
200
43
|
*/
|
|
201
44
|
type: string | undefined
|
|
202
|
-
|
|
203
|
-
|
|
45
|
+
rawOptions: Partial<ast.ParserOptions> | undefined
|
|
46
|
+
options: ast.ParserOptions
|
|
204
47
|
}
|
|
205
48
|
|
|
206
49
|
/**
|
|
207
|
-
*
|
|
50
|
+
* Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
|
|
51
|
+
* enum values into the items sub-schema. This pattern is technically invalid OAS
|
|
52
|
+
* but appears in the wild and must be handled gracefully.
|
|
208
53
|
*/
|
|
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
|
|
54
|
+
function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
55
|
+
const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
|
|
56
|
+
const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
|
|
57
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema
|
|
228
58
|
|
|
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>
|
|
59
|
+
return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
|
|
237
60
|
}
|
|
238
61
|
|
|
239
62
|
/**
|
|
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
|
|
63
|
+
* Builds the internal converter functions for a given `OasParserContext`.
|
|
248
64
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* ```ts
|
|
254
|
-
* const parser = createOasParser(oas)
|
|
255
|
-
* const root = parser.parse({ emptySchemaType: 'unknown' })
|
|
256
|
-
* ```
|
|
65
|
+
* All `convert*` functions are defined as function declarations so they can freely
|
|
66
|
+
* reference each other and `parseSchema` via JS hoisting (mutual recursion).
|
|
257
67
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
68
|
+
function createSchemaParser(ctx: OasParserContext) {
|
|
69
|
+
const document = ctx.document
|
|
70
|
+
|
|
71
|
+
// Branch handlers — each converts one OAS schema pattern to a SchemaNode.
|
|
262
72
|
|
|
263
73
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
74
|
+
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
75
|
+
* recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
|
|
266
76
|
*/
|
|
267
|
-
|
|
268
|
-
if (value === 'any') return schemaTypes.any
|
|
269
|
-
if (value === 'void') return schemaTypes.void
|
|
270
|
-
return schemaTypes.unknown
|
|
271
|
-
}
|
|
77
|
+
const resolvingRefs = new Set<string>()
|
|
272
78
|
|
|
273
79
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
80
|
+
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
81
|
+
*
|
|
82
|
+
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
83
|
+
* (description, readOnly, nullable, etc.) are stored directly on the ref node.
|
|
84
|
+
* Use `syncSchemaRef(node)` in printers to get a merged view of both.
|
|
85
|
+
* Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
|
|
276
86
|
*/
|
|
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 }
|
|
87
|
+
function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
88
|
+
let resolvedSchema: ast.SchemaNode | undefined
|
|
89
|
+
const refPath = schema.$ref
|
|
90
|
+
if (refPath && !resolvingRefs.has(refPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const referenced = resolveRef<SchemaObject>(document, refPath)
|
|
93
|
+
if (referenced) {
|
|
94
|
+
resolvingRefs.add(refPath)
|
|
95
|
+
resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
|
|
96
|
+
resolvingRefs.delete(refPath)
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
294
100
|
}
|
|
295
|
-
return { type: 'datetime', offset: false }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (format === 'date') {
|
|
299
|
-
return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
|
|
300
101
|
}
|
|
301
102
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
return createSchema({
|
|
103
|
+
return ast.createSchema({
|
|
104
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
337
105
|
type: 'ref',
|
|
338
|
-
name: extractRefName(schema.$ref!),
|
|
106
|
+
name: ast.extractRefName(schema.$ref!),
|
|
339
107
|
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,
|
|
108
|
+
schema: resolvedSchema,
|
|
348
109
|
})
|
|
349
110
|
}
|
|
350
111
|
|
|
351
112
|
/**
|
|
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.
|
|
113
|
+
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
365
114
|
*/
|
|
366
|
-
function convertAllOf({ schema, name, nullable, defaultValue,
|
|
115
|
+
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
367
116
|
if (
|
|
368
117
|
schema.allOf!.length === 1 &&
|
|
369
118
|
!schema.properties &&
|
|
@@ -371,12 +120,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
371
120
|
schema.additionalProperties === undefined
|
|
372
121
|
) {
|
|
373
122
|
const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
|
|
374
|
-
const memberNode =
|
|
123
|
+
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
|
|
375
124
|
const { kind: _kind, ...memberNodeProps } = memberNode
|
|
376
125
|
const mergedNullable = nullable || memberNode.nullable || undefined
|
|
377
126
|
const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
|
|
378
127
|
|
|
379
|
-
return createSchema({
|
|
128
|
+
return ast.createSchema({
|
|
380
129
|
...memberNodeProps,
|
|
381
130
|
name,
|
|
382
131
|
title: schema.title ?? memberNode.title,
|
|
@@ -388,31 +137,33 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
388
137
|
default: mergedDefault,
|
|
389
138
|
example: schema.example ?? memberNode.example,
|
|
390
139
|
pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
|
|
391
|
-
} as DistributiveOmit<SchemaNode, 'kind'>)
|
|
140
|
+
} as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
|
|
392
141
|
}
|
|
393
142
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
143
|
+
const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
|
|
144
|
+
const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
397
145
|
.filter((item) => {
|
|
398
146
|
if (!isReference(item) || !name) return true
|
|
399
|
-
const deref =
|
|
147
|
+
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
400
148
|
if (!deref || !isDiscriminator(deref)) return true
|
|
401
149
|
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
402
150
|
if (!parentUnion) return true
|
|
403
151
|
const childRef = `#/components/schemas/${name}`
|
|
404
152
|
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
405
153
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
406
|
-
|
|
154
|
+
if (inOneOf || inMapping) {
|
|
155
|
+
const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef)
|
|
156
|
+
if (discriminatorValue) {
|
|
157
|
+
filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
|
|
158
|
+
}
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
return true
|
|
407
162
|
})
|
|
408
|
-
.map((s) =>
|
|
163
|
+
.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
|
|
409
164
|
|
|
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
165
|
const syntheticStart = allOfMembers.length
|
|
413
166
|
|
|
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
167
|
if (Array.isArray(schema.required) && schema.required.length) {
|
|
417
168
|
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : new Set<string>()
|
|
418
169
|
const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
|
|
@@ -420,14 +171,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
420
171
|
if (missingRequired.length) {
|
|
421
172
|
const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
|
|
422
173
|
if (!isReference(item)) return [item as SchemaObject]
|
|
423
|
-
const deref =
|
|
174
|
+
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
424
175
|
return deref && !isReference(deref) ? [deref] : []
|
|
425
176
|
})
|
|
426
177
|
|
|
427
178
|
for (const key of missingRequired) {
|
|
428
179
|
for (const resolved of resolvedMembers) {
|
|
429
180
|
if (resolved.properties?.[key]) {
|
|
430
|
-
allOfMembers.push(
|
|
181
|
+
allOfMembers.push(parseSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
|
|
431
182
|
break
|
|
432
183
|
}
|
|
433
184
|
}
|
|
@@ -437,110 +188,145 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
437
188
|
|
|
438
189
|
if (schema.properties) {
|
|
439
190
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
440
|
-
allOfMembers.push(
|
|
191
|
+
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const { propertyName, value } of filteredDiscriminantValues) {
|
|
195
|
+
allOfMembers.push(ast.createDiscriminantNode({ propertyName, value }))
|
|
441
196
|
}
|
|
442
197
|
|
|
443
|
-
|
|
444
|
-
return createSchema({
|
|
198
|
+
return ast.createSchema({
|
|
445
199
|
type: 'intersection',
|
|
446
|
-
members: [...allOfMembers.slice(0, syntheticStart), ...
|
|
447
|
-
...
|
|
200
|
+
members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
|
|
201
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
448
202
|
})
|
|
449
203
|
}
|
|
450
204
|
|
|
451
205
|
/**
|
|
452
206
|
* 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
207
|
*/
|
|
459
|
-
function convertUnion({ schema, name, nullable, defaultValue,
|
|
208
|
+
function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
209
|
+
function pickDiscriminatorPropertyNode(node: ast.SchemaNode, propertyName: string): ast.SchemaNode | null {
|
|
210
|
+
const objectNode = ast.narrowSchema(node, 'object')
|
|
211
|
+
const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
|
|
212
|
+
|
|
213
|
+
if (!discriminatorProperty) {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return ast.createSchema({
|
|
218
|
+
type: 'object',
|
|
219
|
+
primitive: 'object',
|
|
220
|
+
properties: [discriminatorProperty],
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
460
224
|
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
461
225
|
const unionBase = {
|
|
462
|
-
...
|
|
226
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
463
227
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
464
228
|
}
|
|
229
|
+
const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
|
|
230
|
+
const sharedPropertiesNode = schema.properties
|
|
231
|
+
? (() => {
|
|
232
|
+
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
233
|
+
const memberBaseSchema: SchemaObject = discriminator
|
|
234
|
+
? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
|
|
235
|
+
: schemaWithoutUnion
|
|
236
|
+
return parseSchema({ schema: memberBaseSchema, name }, rawOptions)
|
|
237
|
+
})()
|
|
238
|
+
: undefined
|
|
465
239
|
|
|
466
|
-
if (
|
|
467
|
-
const
|
|
468
|
-
|
|
240
|
+
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
241
|
+
const members = unionMembers.map((s) => {
|
|
242
|
+
const ref = isReference(s) ? s.$ref : undefined
|
|
243
|
+
const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref)
|
|
244
|
+
const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
|
|
245
|
+
|
|
246
|
+
if (!discriminatorValue || !discriminator) {
|
|
247
|
+
return memberNode
|
|
248
|
+
}
|
|
469
249
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
250
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode
|
|
251
|
+
? pickDiscriminatorPropertyNode(
|
|
252
|
+
ast.setDiscriminatorEnum({
|
|
253
|
+
node: sharedPropertiesNode,
|
|
254
|
+
propertyName: discriminator.propertyName,
|
|
255
|
+
values: [discriminatorValue],
|
|
256
|
+
}),
|
|
257
|
+
discriminator.propertyName,
|
|
258
|
+
)
|
|
259
|
+
: undefined
|
|
260
|
+
|
|
261
|
+
return ast.createSchema({
|
|
262
|
+
type: 'intersection',
|
|
263
|
+
members: [
|
|
264
|
+
memberNode,
|
|
265
|
+
narrowedDiscriminatorNode ?? ast.createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue }),
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
})
|
|
474
269
|
|
|
475
|
-
|
|
270
|
+
const unionNode = ast.createSchema({
|
|
476
271
|
type: 'union',
|
|
477
272
|
...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)
|
|
273
|
+
members,
|
|
274
|
+
})
|
|
483
275
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
276
|
+
if (!sharedPropertiesNode) {
|
|
277
|
+
return unionNode
|
|
278
|
+
}
|
|
487
279
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}),
|
|
280
|
+
return ast.createSchema({
|
|
281
|
+
type: 'intersection',
|
|
282
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
283
|
+
members: [unionNode, sharedPropertiesNode],
|
|
493
284
|
})
|
|
494
285
|
}
|
|
495
286
|
|
|
496
|
-
return createSchema({
|
|
287
|
+
return ast.createSchema({
|
|
497
288
|
type: 'union',
|
|
498
289
|
...unionBase,
|
|
499
|
-
members:
|
|
290
|
+
members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
|
|
500
291
|
})
|
|
501
292
|
}
|
|
502
293
|
|
|
503
294
|
/**
|
|
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.
|
|
295
|
+
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
507
296
|
*/
|
|
508
|
-
function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
297
|
+
function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
509
298
|
const constValue = schema.const
|
|
510
299
|
|
|
511
300
|
if (constValue === null) {
|
|
512
|
-
return createSchema({
|
|
301
|
+
return ast.createSchema({
|
|
513
302
|
type: 'null',
|
|
514
303
|
primitive: 'null',
|
|
515
304
|
name,
|
|
516
305
|
title: schema.title,
|
|
517
306
|
description: schema.description,
|
|
518
307
|
deprecated: schema.deprecated,
|
|
519
|
-
nullable,
|
|
520
308
|
})
|
|
521
309
|
}
|
|
522
310
|
|
|
523
311
|
const constPrimitive = getPrimitiveType(typeof constValue === 'number' ? 'number' : typeof constValue === 'boolean' ? 'boolean' : 'string')
|
|
524
|
-
return createSchema({
|
|
312
|
+
return ast.createSchema({
|
|
525
313
|
type: 'enum',
|
|
526
314
|
primitive: constPrimitive,
|
|
527
315
|
enumValues: [constValue as string | number | boolean],
|
|
528
|
-
...
|
|
316
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
529
317
|
})
|
|
530
318
|
}
|
|
531
319
|
|
|
532
320
|
/**
|
|
533
|
-
*
|
|
534
|
-
* Returns `
|
|
535
|
-
* (i.e. `format: 'date-time'` with `dateType: false`).
|
|
321
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
322
|
+
* Returns `null` when the format should fall through to string handling (`dateType: false`).
|
|
536
323
|
*/
|
|
537
|
-
function convertFormat({ schema, name, nullable, defaultValue,
|
|
538
|
-
const base =
|
|
324
|
+
function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): ast.SchemaNode | null {
|
|
325
|
+
const base = buildSchemaNode(schema, name, nullable, defaultValue)
|
|
539
326
|
|
|
540
|
-
// int64 is option-dependent so it can't live in the static formatMap.
|
|
541
327
|
if (schema.format === 'int64') {
|
|
542
|
-
return createSchema({
|
|
543
|
-
type:
|
|
328
|
+
return ast.createSchema({
|
|
329
|
+
type: options.integerType === 'bigint' ? 'bigint' : 'integer',
|
|
544
330
|
primitive: 'integer',
|
|
545
331
|
...base,
|
|
546
332
|
min: schema.minimum,
|
|
@@ -550,48 +336,48 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
550
336
|
})
|
|
551
337
|
}
|
|
552
338
|
|
|
553
|
-
// date-time / date / time are option-dependent and can't live in the static formatMap.
|
|
554
339
|
if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
|
|
555
|
-
const dateType = getDateType(
|
|
556
|
-
if (!dateType) return
|
|
340
|
+
const dateType = getDateType(options, schema.format)
|
|
341
|
+
if (!dateType) return null
|
|
557
342
|
|
|
558
343
|
if (dateType.type === 'datetime') {
|
|
559
|
-
return createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
|
|
344
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
|
|
560
345
|
}
|
|
561
|
-
return createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
|
|
346
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
|
|
562
347
|
}
|
|
563
348
|
|
|
564
|
-
const specialType =
|
|
565
|
-
if (!specialType) return
|
|
349
|
+
const specialType = getSchemaType(schema.format!)
|
|
350
|
+
if (!specialType) return null
|
|
566
351
|
|
|
567
|
-
const specialPrimitive: PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
|
|
352
|
+
const specialPrimitive: ast.PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
|
|
568
353
|
|
|
569
354
|
if (specialType === 'number' || specialType === 'integer' || specialType === 'bigint') {
|
|
570
|
-
return createSchema({ ...base, primitive: specialPrimitive, type: specialType })
|
|
355
|
+
return ast.createSchema({ ...base, primitive: specialPrimitive, type: specialType })
|
|
356
|
+
}
|
|
357
|
+
if (specialType === 'url') {
|
|
358
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: 'url', min: schema.minLength, max: schema.maxLength })
|
|
359
|
+
}
|
|
360
|
+
if (specialType === 'ipv4') {
|
|
361
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: 'ipv4' })
|
|
362
|
+
}
|
|
363
|
+
if (specialType === 'ipv6') {
|
|
364
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: 'ipv6' })
|
|
571
365
|
}
|
|
572
|
-
|
|
366
|
+
if (specialType === 'uuid' || specialType === 'email') {
|
|
367
|
+
return ast.createSchema({ ...base, primitive: 'string' as const, type: specialType, min: schema.minLength, max: schema.maxLength })
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return ast.createSchema({ ...base, primitive: specialPrimitive, type: specialType as ast.ScalarSchemaType })
|
|
573
371
|
}
|
|
574
372
|
|
|
575
373
|
/**
|
|
576
374
|
* 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
375
|
*/
|
|
585
|
-
function convertEnum({ schema, name, nullable, type,
|
|
586
|
-
// Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
|
|
376
|
+
function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
587
377
|
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)
|
|
378
|
+
return parseSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
|
|
592
379
|
}
|
|
593
380
|
|
|
594
|
-
// `null` in enum values is the OAS 3.0 convention for a nullable enum.
|
|
595
381
|
const nullInEnum = schema.enum!.includes(null)
|
|
596
382
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
597
383
|
const enumNullable = nullable || nullInEnum || undefined
|
|
@@ -612,93 +398,60 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
612
398
|
example: schema.example,
|
|
613
399
|
}
|
|
614
400
|
|
|
615
|
-
// x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
|
|
616
401
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
617
|
-
if (extensionKey) {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return createSchema({
|
|
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)]
|
|
410
|
+
|
|
411
|
+
return ast.createSchema({
|
|
628
412
|
...enumBase,
|
|
629
|
-
|
|
630
|
-
namedEnumValues:
|
|
413
|
+
primitive: enumPrimitiveType,
|
|
414
|
+
namedEnumValues: sourceValues.map((label, index) => ({
|
|
631
415
|
name: String(label),
|
|
632
|
-
value: filteredValues[index] ?? label,
|
|
633
|
-
|
|
416
|
+
value: extensionKey ? (filteredValues[index] ?? label) : label,
|
|
417
|
+
primitive: enumPrimitiveType,
|
|
634
418
|
})),
|
|
635
419
|
})
|
|
636
420
|
}
|
|
637
421
|
|
|
638
|
-
|
|
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,
|
|
660
|
-
})),
|
|
661
|
-
})
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Plain string enum (default path).
|
|
665
|
-
return createSchema({
|
|
422
|
+
return ast.createSchema({
|
|
666
423
|
...enumBase,
|
|
667
424
|
enumValues: [...new Set(filteredValues)],
|
|
668
425
|
})
|
|
669
426
|
}
|
|
670
427
|
|
|
671
428
|
/**
|
|
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`
|
|
429
|
+
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
682
430
|
*/
|
|
683
|
-
function convertObject({ schema, name, nullable, defaultValue,
|
|
684
|
-
const properties: Array<PropertyNode> = schema.properties
|
|
431
|
+
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
432
|
+
const properties: Array<ast.PropertyNode> = schema.properties
|
|
685
433
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
686
434
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
687
435
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
688
436
|
const propNullable = isNullable(resolvedPropSchema)
|
|
689
|
-
const basePropName = name ? pascalCase([name, propName].join(' ')) : undefined
|
|
690
|
-
const propNode = convertSchema({ schema: resolvedPropSchema, name: basePropName }, options)
|
|
691
|
-
const isEnumNode = !!narrowSchema(propNode, 'enum')
|
|
692
|
-
const derivedPropName = isEnumNode && name ? pascalCase([name, propName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : basePropName
|
|
693
|
-
const schemaNode = isEnumNode && derivedPropName !== basePropName ? { ...propNode, name: derivedPropName } : propNode
|
|
694
437
|
|
|
695
|
-
|
|
438
|
+
const resolvedChildName = ast.childName(name, propName)
|
|
439
|
+
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
440
|
+
let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix)
|
|
441
|
+
|
|
442
|
+
const tupleNode = ast.narrowSchema(schemaNode, 'tuple')
|
|
443
|
+
if (tupleNode?.items) {
|
|
444
|
+
const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
|
|
445
|
+
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
446
|
+
schemaNode = { ...tupleNode, items: namedItems }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return ast.createProperty({
|
|
696
451
|
name: propName,
|
|
697
452
|
schema: {
|
|
698
453
|
...schemaNode,
|
|
699
|
-
nullable: propNullable || undefined,
|
|
700
|
-
optional: !required && !propNullable ? true : undefined,
|
|
701
|
-
nullish: !required && propNullable ? true : undefined,
|
|
454
|
+
nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
|
|
702
455
|
},
|
|
703
456
|
required,
|
|
704
457
|
})
|
|
@@ -706,15 +459,15 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
706
459
|
: []
|
|
707
460
|
|
|
708
461
|
const additionalProperties = schema.additionalProperties
|
|
709
|
-
let additionalPropertiesNode: SchemaNode |
|
|
462
|
+
let additionalPropertiesNode: ast.SchemaNode | boolean | undefined
|
|
710
463
|
if (additionalProperties === true) {
|
|
711
464
|
additionalPropertiesNode = true
|
|
712
465
|
} else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
713
|
-
additionalPropertiesNode =
|
|
466
|
+
additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
714
467
|
} else if (additionalProperties === false) {
|
|
715
|
-
additionalPropertiesNode =
|
|
468
|
+
additionalPropertiesNode = false
|
|
716
469
|
} else if (additionalProperties) {
|
|
717
|
-
additionalPropertiesNode = createSchema({ type:
|
|
470
|
+
additionalPropertiesNode = ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
718
471
|
}
|
|
719
472
|
|
|
720
473
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
@@ -724,28 +477,28 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
724
477
|
Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
|
|
725
478
|
pattern,
|
|
726
479
|
patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
|
|
727
|
-
? createSchema({ type:
|
|
728
|
-
:
|
|
480
|
+
? ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
481
|
+
: parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
|
|
729
482
|
]),
|
|
730
483
|
)
|
|
731
484
|
: undefined
|
|
732
485
|
|
|
733
|
-
const objectNode: SchemaNode = createSchema({
|
|
486
|
+
const objectNode: ast.SchemaNode = ast.createSchema({
|
|
734
487
|
type: 'object',
|
|
735
488
|
primitive: 'object',
|
|
736
489
|
properties,
|
|
737
490
|
additionalProperties: additionalPropertiesNode,
|
|
738
491
|
patternProperties,
|
|
739
|
-
|
|
492
|
+
minProperties: schema.minProperties,
|
|
493
|
+
maxProperties: schema.maxProperties,
|
|
494
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
740
495
|
})
|
|
741
496
|
|
|
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
497
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
745
498
|
const discPropName = schema.discriminator.propertyName
|
|
746
499
|
const values = Object.keys(schema.discriminator.mapping)
|
|
747
|
-
const enumName = name ?
|
|
748
|
-
return
|
|
500
|
+
const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
|
|
501
|
+
return ast.setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
|
|
749
502
|
}
|
|
750
503
|
|
|
751
504
|
return objectNode
|
|
@@ -753,94 +506,87 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
753
506
|
|
|
754
507
|
/**
|
|
755
508
|
* 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
509
|
*/
|
|
760
|
-
function convertTuple({ schema, name, nullable, defaultValue,
|
|
761
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) =>
|
|
762
|
-
const rest = schema.items ?
|
|
510
|
+
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
511
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
|
|
512
|
+
const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : ast.createSchema({ type: 'any' })
|
|
763
513
|
|
|
764
|
-
return createSchema({
|
|
514
|
+
return ast.createSchema({
|
|
765
515
|
type: 'tuple',
|
|
766
516
|
primitive: 'array',
|
|
767
517
|
items: tupleItems,
|
|
768
518
|
rest,
|
|
769
519
|
min: schema.minItems,
|
|
770
520
|
max: schema.maxItems,
|
|
771
|
-
...
|
|
521
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
772
522
|
})
|
|
773
523
|
}
|
|
774
524
|
|
|
775
525
|
/**
|
|
776
526
|
* 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
527
|
*/
|
|
781
|
-
function convertArray({ schema, name, nullable, defaultValue,
|
|
528
|
+
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
782
529
|
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)] : []
|
|
530
|
+
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(undefined, name, options.enumSuffix) : undefined
|
|
531
|
+
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
787
532
|
|
|
788
|
-
return createSchema({
|
|
533
|
+
return ast.createSchema({
|
|
789
534
|
type: 'array',
|
|
790
535
|
primitive: 'array',
|
|
791
536
|
items,
|
|
792
537
|
min: schema.minItems,
|
|
793
538
|
max: schema.maxItems,
|
|
794
539
|
unique: schema.uniqueItems ?? undefined,
|
|
795
|
-
...
|
|
540
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
796
541
|
})
|
|
797
542
|
}
|
|
798
543
|
|
|
799
544
|
/**
|
|
800
|
-
* Converts a `type: 'string'` schema
|
|
545
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
801
546
|
*/
|
|
802
|
-
function convertString({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
803
|
-
return createSchema({
|
|
547
|
+
function convertString({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
548
|
+
return ast.createSchema({
|
|
804
549
|
type: 'string',
|
|
805
550
|
primitive: 'string',
|
|
806
551
|
min: schema.minLength,
|
|
807
552
|
max: schema.maxLength,
|
|
808
553
|
pattern: schema.pattern,
|
|
809
|
-
...
|
|
554
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
810
555
|
})
|
|
811
556
|
}
|
|
812
557
|
|
|
813
558
|
/**
|
|
814
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema
|
|
559
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
815
560
|
*/
|
|
816
|
-
function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): SchemaNode {
|
|
817
|
-
return createSchema({
|
|
561
|
+
function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): ast.SchemaNode {
|
|
562
|
+
return ast.createSchema({
|
|
818
563
|
type,
|
|
819
564
|
primitive: type,
|
|
820
565
|
min: schema.minimum,
|
|
821
566
|
max: schema.maximum,
|
|
822
567
|
exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
|
|
823
568
|
exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
|
|
824
|
-
|
|
569
|
+
multipleOf: schema.multipleOf,
|
|
570
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
825
571
|
})
|
|
826
572
|
}
|
|
827
573
|
|
|
828
574
|
/**
|
|
829
|
-
* Converts a `type: 'boolean'` schema
|
|
575
|
+
* Converts a `type: 'boolean'` schema.
|
|
830
576
|
*/
|
|
831
|
-
function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
832
|
-
return createSchema({
|
|
577
|
+
function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
578
|
+
return ast.createSchema({
|
|
833
579
|
type: 'boolean',
|
|
834
580
|
primitive: 'boolean',
|
|
835
|
-
...
|
|
581
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
836
582
|
})
|
|
837
583
|
}
|
|
838
584
|
|
|
839
585
|
/**
|
|
840
|
-
* Converts an explicit `type: 'null'`
|
|
586
|
+
* Converts an explicit `type: 'null'` schema.
|
|
841
587
|
*/
|
|
842
|
-
function convertNull({ schema, name, nullable }: SchemaContext): SchemaNode {
|
|
843
|
-
return createSchema({
|
|
588
|
+
function convertNull({ schema, name, nullable }: SchemaContext): ast.SchemaNode {
|
|
589
|
+
return ast.createSchema({
|
|
844
590
|
type: 'null',
|
|
845
591
|
primitive: 'null',
|
|
846
592
|
name,
|
|
@@ -852,83 +598,55 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
852
598
|
}
|
|
853
599
|
|
|
854
600
|
/**
|
|
855
|
-
* Central dispatcher
|
|
601
|
+
* Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
856
602
|
*
|
|
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)
|
|
603
|
+
* Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
|
|
604
|
+
* → octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
|
|
605
|
+
* → empty-schema fallback (`emptySchemaType` option).
|
|
869
606
|
*/
|
|
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.
|
|
607
|
+
function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
|
|
608
|
+
const options: ast.ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
|
|
874
609
|
const flattenedSchema = flattenSchema(schema)
|
|
875
610
|
if (flattenedSchema && flattenedSchema !== schema) {
|
|
876
|
-
return
|
|
611
|
+
return parseSchema({ schema: flattenedSchema, name }, rawOptions)
|
|
877
612
|
}
|
|
878
613
|
|
|
879
614
|
const nullable = isNullable(schema) || undefined
|
|
880
615
|
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
616
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
883
617
|
|
|
884
|
-
const ctx: SchemaContext = { schema, name, nullable, defaultValue, type,
|
|
618
|
+
const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
|
|
885
619
|
|
|
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
620
|
if (isReference(schema)) return convertRef(ctx)
|
|
890
621
|
|
|
891
|
-
// Composition keywords
|
|
892
622
|
if (schema.allOf?.length) return convertAllOf(ctx)
|
|
893
623
|
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
894
624
|
if (unionMembers.length) return convertUnion(ctx)
|
|
895
625
|
|
|
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
626
|
if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
|
|
899
627
|
|
|
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
628
|
if (schema.format) {
|
|
904
629
|
const formatResult = convertFormat(ctx)
|
|
905
630
|
if (formatResult) return formatResult
|
|
906
631
|
}
|
|
907
632
|
|
|
908
|
-
// OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
|
|
909
633
|
if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
|
|
910
|
-
return createSchema({ type: 'blob', primitive: 'string', ...
|
|
634
|
+
return ast.createSchema({ type: 'blob', primitive: 'string', ...buildSchemaNode(schema, name, nullable, defaultValue) })
|
|
911
635
|
}
|
|
912
636
|
|
|
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
637
|
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
917
638
|
const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
|
|
918
639
|
const arrayNullable = schema.type.includes('null') || nullable || undefined
|
|
919
640
|
|
|
920
641
|
if (nonNullTypes.length > 1) {
|
|
921
|
-
return createSchema({
|
|
642
|
+
return ast.createSchema({
|
|
922
643
|
type: 'union',
|
|
923
|
-
members: nonNullTypes.map((t) =>
|
|
924
|
-
...
|
|
644
|
+
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
645
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
925
646
|
})
|
|
926
647
|
}
|
|
927
648
|
}
|
|
928
649
|
|
|
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
650
|
if (!type) {
|
|
933
651
|
if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
|
|
934
652
|
return convertString(ctx)
|
|
@@ -948,52 +666,84 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
948
666
|
if (type === 'boolean') return convertBoolean(ctx)
|
|
949
667
|
if (type === 'null') return convertNull(ctx)
|
|
950
668
|
|
|
951
|
-
const emptyType =
|
|
952
|
-
return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
|
|
669
|
+
const emptyType = typeOptionMap.get(options.emptySchemaType)!
|
|
670
|
+
return ast.createSchema({ type: emptyType as ast.ScalarSchemaType, name, title: schema.title, description: schema.description })
|
|
953
671
|
}
|
|
954
672
|
|
|
955
673
|
/**
|
|
956
|
-
* Converts a
|
|
957
|
-
* When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
|
|
674
|
+
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
958
675
|
*/
|
|
959
|
-
function parseParameter(options:
|
|
676
|
+
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>): ast.ParameterNode {
|
|
960
677
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
961
678
|
|
|
962
|
-
const schema: SchemaNode =
|
|
963
|
-
param['schema']
|
|
964
|
-
|
|
965
|
-
: createSchema({ type: resolveTypeOption(options.unknownType) })
|
|
679
|
+
const schema: ast.SchemaNode = param['schema']
|
|
680
|
+
? parseSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
681
|
+
: ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
966
682
|
|
|
967
|
-
return createParameter({
|
|
683
|
+
return ast.createParameter({
|
|
968
684
|
name: param['name'] as string,
|
|
969
|
-
in: param['in'] as ParameterLocation,
|
|
685
|
+
in: param['in'] as ast.ParameterLocation,
|
|
970
686
|
schema: {
|
|
971
687
|
...schema,
|
|
972
|
-
|
|
688
|
+
description: (param['description'] as string | undefined) ?? schema.description,
|
|
973
689
|
},
|
|
974
690
|
required,
|
|
975
691
|
})
|
|
976
692
|
}
|
|
977
693
|
|
|
978
694
|
/**
|
|
979
|
-
* Converts an OAS `Operation` into an `OperationNode
|
|
980
|
-
* request body, and all response codes into their AST node equivalents.
|
|
695
|
+
* Converts an OAS `Operation` into an `OperationNode`.
|
|
981
696
|
*/
|
|
982
|
-
function parseOperation(options:
|
|
983
|
-
const parameters: Array<ParameterNode> =
|
|
984
|
-
|
|
697
|
+
function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
|
|
698
|
+
const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
|
|
699
|
+
parseParameter(options, param as unknown as Record<string, unknown>),
|
|
700
|
+
)
|
|
985
701
|
|
|
986
|
-
|
|
987
|
-
})
|
|
702
|
+
const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
|
|
703
|
+
const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
|
|
988
704
|
|
|
989
|
-
const
|
|
990
|
-
|
|
705
|
+
const requestBodyDescription =
|
|
706
|
+
operation.schema.requestBody && !isReference(operation.schema.requestBody)
|
|
707
|
+
? (operation.schema.requestBody as { description?: string }).description
|
|
708
|
+
: undefined
|
|
991
709
|
|
|
992
|
-
const
|
|
710
|
+
const requestBodyKeysToOmit = requestBodySchema?.properties
|
|
711
|
+
? Object.entries(requestBodySchema.properties)
|
|
712
|
+
.filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
|
|
713
|
+
.map(([key]) => key)
|
|
714
|
+
: undefined
|
|
715
|
+
|
|
716
|
+
const requestBodyRequired =
|
|
717
|
+
operation.schema.requestBody && !isReference(operation.schema.requestBody)
|
|
718
|
+
? (operation.schema.requestBody as { required?: boolean }).required === true
|
|
719
|
+
: false
|
|
720
|
+
|
|
721
|
+
const requestBodyContentType = (() => {
|
|
722
|
+
if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) {
|
|
723
|
+
return undefined
|
|
724
|
+
}
|
|
725
|
+
const content = (operation.schema.requestBody as { content?: Record<string, unknown> }).content
|
|
726
|
+
return content ? Object.keys(content)[0] : undefined
|
|
727
|
+
})()
|
|
728
|
+
|
|
729
|
+
const requestBody = requestBodySchemaNode
|
|
730
|
+
? {
|
|
731
|
+
description: requestBodyDescription,
|
|
732
|
+
schema: ast.syncOptionality(requestBodySchemaNode, requestBodyRequired),
|
|
733
|
+
keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
|
|
734
|
+
required: requestBodyRequired || undefined,
|
|
735
|
+
contentType: requestBodyContentType,
|
|
736
|
+
}
|
|
737
|
+
: undefined
|
|
738
|
+
|
|
739
|
+
const responses: Array<ast.ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
|
|
993
740
|
const responseObj = operation.getResponseByStatusCode(statusCode)
|
|
994
|
-
const responseSchema =
|
|
741
|
+
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
|
|
995
742
|
|
|
996
|
-
const schema =
|
|
743
|
+
const schema =
|
|
744
|
+
responseSchema && Object.keys(responseSchema).length > 0
|
|
745
|
+
? parseSchema({ schema: responseSchema }, options)
|
|
746
|
+
: ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
997
747
|
|
|
998
748
|
const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
|
|
999
749
|
|
|
@@ -1002,20 +752,29 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1002
752
|
? (responseObj as { content?: Record<string, unknown> }).content
|
|
1003
753
|
: undefined
|
|
1004
754
|
|
|
1005
|
-
const mediaType = rawContent ?
|
|
755
|
+
const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
|
|
756
|
+
|
|
757
|
+
const keysToOmit = responseSchema?.properties
|
|
758
|
+
? Object.entries(responseSchema.properties)
|
|
759
|
+
.filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
|
|
760
|
+
.map(([key]) => key)
|
|
761
|
+
: undefined
|
|
1006
762
|
|
|
1007
|
-
return createResponse({
|
|
1008
|
-
statusCode: statusCode as StatusCode,
|
|
763
|
+
return ast.createResponse({
|
|
764
|
+
statusCode: statusCode as ast.StatusCode,
|
|
1009
765
|
description,
|
|
1010
766
|
schema,
|
|
1011
767
|
mediaType,
|
|
768
|
+
keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
|
|
1012
769
|
})
|
|
1013
770
|
})
|
|
1014
771
|
|
|
1015
|
-
|
|
772
|
+
const urlPath = new URLPath(operation.path)
|
|
773
|
+
|
|
774
|
+
return ast.createOperation({
|
|
1016
775
|
operationId: operation.getOperationId(),
|
|
1017
|
-
method: operation.method.toUpperCase() as HttpMethod,
|
|
1018
|
-
path:
|
|
776
|
+
method: operation.method.toUpperCase() as ast.HttpMethod,
|
|
777
|
+
path: urlPath.path,
|
|
1019
778
|
tags: operation.getTags().map((tag) => tag.name),
|
|
1020
779
|
summary: operation.getSummary() || undefined,
|
|
1021
780
|
description: operation.getDescription() || undefined,
|
|
@@ -1026,63 +785,61 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1026
785
|
})
|
|
1027
786
|
}
|
|
1028
787
|
|
|
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 }
|
|
788
|
+
return { parseSchema, parseOperation, parseParameter }
|
|
789
|
+
}
|
|
1035
790
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Converts a single `SchemaObject` into a `SchemaNode`.
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```ts
|
|
796
|
+
* const ctx = { document }
|
|
797
|
+
* parseSchema(ctx, { schema: { type: 'string', format: 'uuid' } })
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
export function parseSchema(
|
|
801
|
+
ctx: OasParserContext,
|
|
802
|
+
{ schema, name }: { schema: SchemaObject; name?: string },
|
|
803
|
+
options?: Partial<ast.ParserOptions>,
|
|
804
|
+
): ast.SchemaNode {
|
|
805
|
+
return createSchemaParser(ctx).parseSchema({ schema, name }, options)
|
|
806
|
+
}
|
|
1039
807
|
|
|
1040
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Converts the entire OpenAPI spec into an `InputNode` (the top-level `@kubb/ast` tree).
|
|
810
|
+
*
|
|
811
|
+
* This is the main entry point: `OpenAPI / Swagger → Kubb AST`.
|
|
812
|
+
* No code is generated here — the resulting tree is spec-agnostic and consumed by
|
|
813
|
+
* downstream plugins (`plugin-ts`, `plugin-zod`, …).
|
|
814
|
+
*
|
|
815
|
+
* @example
|
|
816
|
+
* ```ts
|
|
817
|
+
* const document = await parseFromConfig(config)
|
|
818
|
+
* const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
export function parseOas(
|
|
822
|
+
document: Document,
|
|
823
|
+
options: Partial<ast.ParserOptions> & { contentType?: ContentType } = {},
|
|
824
|
+
): { root: ast.InputNode; nameMapping: Map<string, string> } {
|
|
825
|
+
const { contentType, ...parserOptions } = options
|
|
826
|
+
const mergedOptions: ast.ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...parserOptions }
|
|
1041
827
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
.map(([, operation]) => (operation ? parseOperation(mergedOptions, oas, operation) : null))
|
|
1045
|
-
.filter((op): op is OperationNode => op !== null),
|
|
1046
|
-
)
|
|
828
|
+
const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType })
|
|
829
|
+
const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
|
|
1047
830
|
|
|
1048
|
-
|
|
1049
|
-
}
|
|
831
|
+
const schemas: Array<ast.SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
|
|
1050
832
|
|
|
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
|
-
}
|
|
833
|
+
const baseOas = new BaseOas(document)
|
|
834
|
+
const paths = baseOas.getPaths()
|
|
1071
835
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}) as SchemaNode
|
|
1080
|
-
}
|
|
836
|
+
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
|
|
837
|
+
Object.entries(methods)
|
|
838
|
+
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
839
|
+
.filter((op): op is ast.OperationNode => op !== null),
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
const root = ast.createInput({ schemas, operations })
|
|
1081
843
|
|
|
1082
|
-
return {
|
|
1083
|
-
parse,
|
|
1084
|
-
convertSchema,
|
|
1085
|
-
resolveRefs,
|
|
1086
|
-
nameMapping,
|
|
1087
|
-
} as OasParser
|
|
844
|
+
return { root, nameMapping }
|
|
1088
845
|
}
|