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