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