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