@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.60
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/LICENSE +17 -10
- package/README.md +100 -0
- package/dist/index.cjs +1451 -614
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +821 -63
- package/dist/index.js +1453 -611
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
- package/src/adapter.bench.ts +60 -0
- package/src/adapter.ts +133 -56
- package/src/bundler.ts +71 -0
- package/src/constants.ts +17 -3
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +68 -44
- package/src/factory.ts +63 -48
- package/src/index.ts +0 -3
- package/src/mime.ts +22 -0
- package/src/operation.ts +194 -0
- package/src/parser.ts +321 -203
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +86 -48
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +283 -0
- package/src/types.ts +82 -53
- package/extension.yaml +0 -344
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/parser.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pascalCase } from '@internals/utils'
|
|
2
|
+
import { childName, enumPropName, extractRefName } from '@kubb/ast/utils'
|
|
2
3
|
import { ast } from '@kubb/core'
|
|
3
|
-
import BaseOas from 'oas'
|
|
4
4
|
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, SCHEMA_REF_PREFIX, typeOptionMap } from './constants.ts'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { oasDialect, type OasDialect } from './dialect.ts'
|
|
6
|
+
import { createDiscriminantNode, findDiscriminator } from './discriminator.ts'
|
|
7
|
+
import { getOperationId, getOperations, getRequestContentType, getResponseByStatusCode, getResponseStatusCodes } from './operation.ts'
|
|
7
8
|
import {
|
|
8
9
|
buildSchemaNode,
|
|
9
10
|
flattenSchema,
|
|
10
11
|
getDateType,
|
|
11
|
-
getMediaType,
|
|
12
12
|
getParameters,
|
|
13
13
|
getPrimitiveType,
|
|
14
14
|
getRequestBodyContentTypes,
|
|
15
15
|
getRequestSchema,
|
|
16
|
+
getResponseBodyContentTypes,
|
|
16
17
|
getResponseSchema,
|
|
17
18
|
getSchemas,
|
|
18
19
|
getSchemaType,
|
|
@@ -30,6 +31,16 @@ export type OasParserContext = {
|
|
|
30
31
|
contentType?: ContentType
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The object returned by {@link createSchemaParser}.
|
|
36
|
+
* Contains parser functions bound to a specific document.
|
|
37
|
+
*/
|
|
38
|
+
export type SchemaParser = {
|
|
39
|
+
parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
|
|
40
|
+
parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
|
|
41
|
+
parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Pre-computed per-schema context passed to every schema converter.
|
|
35
46
|
*
|
|
@@ -50,13 +61,33 @@ type SchemaContext = {
|
|
|
50
61
|
options: ast.ParserOptions
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
/**
|
|
65
|
+
* One entry in the ordered schema rule table: a predicate paired with a converter. A rule whose
|
|
66
|
+
* `match` returns `true` may still `convert` to `null` to defer to the next rule (e.g. a `format`
|
|
67
|
+
* that is not convertible falls through to plain `type` handling).
|
|
68
|
+
*/
|
|
69
|
+
type SchemaRule = {
|
|
70
|
+
/**
|
|
71
|
+
* Identifies the rule when reading the table or debugging which branch ran.
|
|
72
|
+
*/
|
|
73
|
+
name: string
|
|
74
|
+
/**
|
|
75
|
+
* Returns `true` when this rule is responsible for the given context.
|
|
76
|
+
*/
|
|
77
|
+
match: (context: SchemaContext) => boolean
|
|
78
|
+
/**
|
|
79
|
+
* Produces a node for the context, or `null` to fall through to the next rule.
|
|
80
|
+
*/
|
|
81
|
+
convert: (context: SchemaContext) => ast.SchemaNode | null
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
/**
|
|
54
85
|
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
55
86
|
*
|
|
56
87
|
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
57
|
-
* from the array to its items sub-schema,
|
|
88
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
58
89
|
*
|
|
59
|
-
* @note
|
|
90
|
+
* @note A defensive measure for non-compliant specs.
|
|
60
91
|
*/
|
|
61
92
|
function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
62
93
|
const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
|
|
@@ -74,14 +105,14 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
|
74
105
|
*
|
|
75
106
|
* Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
|
|
76
107
|
* Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
|
|
77
|
-
*
|
|
108
|
+
* which works because function declarations hoist.
|
|
78
109
|
*
|
|
79
|
-
* @
|
|
110
|
+
* @internal
|
|
80
111
|
*/
|
|
81
|
-
function createSchemaParser(ctx: OasParserContext) {
|
|
112
|
+
export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
|
|
82
113
|
const document = ctx.document
|
|
83
114
|
|
|
84
|
-
// Branch handlers
|
|
115
|
+
// Branch handlers, each converts one OAS schema pattern to a SchemaNode.
|
|
85
116
|
|
|
86
117
|
/**
|
|
87
118
|
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
@@ -89,6 +120,16 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
89
120
|
*/
|
|
90
121
|
const resolvingRefs = new Set<string>()
|
|
91
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
125
|
+
*
|
|
126
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
127
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
128
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
129
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
130
|
+
*/
|
|
131
|
+
const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
|
|
132
|
+
|
|
92
133
|
/**
|
|
93
134
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
94
135
|
*
|
|
@@ -98,25 +139,29 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
98
139
|
* Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
|
|
99
140
|
*/
|
|
100
141
|
function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
101
|
-
let resolvedSchema: ast.SchemaNode |
|
|
142
|
+
let resolvedSchema: ast.SchemaNode | null = null
|
|
102
143
|
const refPath = schema.$ref
|
|
103
144
|
if (refPath && !resolvingRefs.has(refPath)) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
145
|
+
if (!resolvedRefCache.has(refPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
|
|
148
|
+
if (referenced) {
|
|
149
|
+
resolvingRefs.add(refPath)
|
|
150
|
+
resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
|
|
151
|
+
resolvingRefs.delete(refPath)
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
110
155
|
}
|
|
111
|
-
|
|
112
|
-
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
156
|
+
resolvedRefCache.set(refPath, resolvedSchema)
|
|
113
157
|
}
|
|
158
|
+
resolvedSchema = resolvedRefCache.get(refPath) ?? null
|
|
114
159
|
}
|
|
115
160
|
|
|
116
|
-
return ast.createSchema({
|
|
161
|
+
return ast.factory.createSchema({
|
|
117
162
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
118
163
|
type: 'ref',
|
|
119
|
-
name:
|
|
164
|
+
name: extractRefName(schema.$ref!),
|
|
120
165
|
ref: schema.$ref,
|
|
121
166
|
schema: resolvedSchema,
|
|
122
167
|
})
|
|
@@ -133,12 +178,12 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
133
178
|
schema.additionalProperties === undefined
|
|
134
179
|
) {
|
|
135
180
|
const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
|
|
136
|
-
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name
|
|
181
|
+
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name }, rawOptions)
|
|
137
182
|
const { kind: _kind, ...memberNodeProps } = memberNode
|
|
138
183
|
const mergedNullable = nullable || memberNode.nullable || undefined
|
|
139
184
|
const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
|
|
140
185
|
|
|
141
|
-
return ast.createSchema({
|
|
186
|
+
return ast.factory.createSchema({
|
|
142
187
|
...memberNodeProps,
|
|
143
188
|
name,
|
|
144
189
|
title: schema.title ?? memberNode.title,
|
|
@@ -150,6 +195,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
150
195
|
default: mergedDefault,
|
|
151
196
|
example: schema.example ?? memberNode.example,
|
|
152
197
|
pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
|
|
198
|
+
format: schema.format ?? memberNode.format,
|
|
153
199
|
} as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
|
|
154
200
|
}
|
|
155
201
|
|
|
@@ -159,16 +205,16 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
159
205
|
}> = []
|
|
160
206
|
const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
161
207
|
.filter((item) => {
|
|
162
|
-
if (!isReference(item) || !name) return true
|
|
163
|
-
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
164
|
-
if (!deref || !isDiscriminator(deref)) return true
|
|
208
|
+
if (!dialect.isReference(item) || !name) return true
|
|
209
|
+
const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
|
|
210
|
+
if (!deref || !dialect.isDiscriminator(deref)) return true
|
|
165
211
|
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
166
212
|
if (!parentUnion) return true
|
|
167
213
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`
|
|
168
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
214
|
+
const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
169
215
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
170
216
|
if (inOneOf || inMapping) {
|
|
171
|
-
const discriminatorValue =
|
|
217
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
|
|
172
218
|
if (discriminatorValue) {
|
|
173
219
|
filteredDiscriminantValues.push({
|
|
174
220
|
propertyName: deref.discriminator.propertyName,
|
|
@@ -179,7 +225,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
179
225
|
}
|
|
180
226
|
return true
|
|
181
227
|
})
|
|
182
|
-
.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
|
|
228
|
+
.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))
|
|
183
229
|
|
|
184
230
|
const syntheticStart = allOfMembers.length
|
|
185
231
|
|
|
@@ -189,9 +235,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
189
235
|
|
|
190
236
|
if (missingRequired.length) {
|
|
191
237
|
const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
|
|
192
|
-
if (!isReference(item)) return [item as SchemaObject]
|
|
193
|
-
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
194
|
-
return deref && !isReference(deref) ? [deref] : []
|
|
238
|
+
if (!dialect.isReference(item)) return [item as SchemaObject]
|
|
239
|
+
const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
|
|
240
|
+
return deref && !dialect.isReference(deref) ? [deref] : []
|
|
195
241
|
})
|
|
196
242
|
|
|
197
243
|
for (const key of missingRequired) {
|
|
@@ -204,6 +250,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
204
250
|
properties: { [key]: resolved.properties[key] },
|
|
205
251
|
required: [key],
|
|
206
252
|
} as SchemaObject,
|
|
253
|
+
name,
|
|
207
254
|
},
|
|
208
255
|
rawOptions,
|
|
209
256
|
),
|
|
@@ -217,16 +264,19 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
217
264
|
|
|
218
265
|
if (schema.properties) {
|
|
219
266
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
267
|
+
// Don't pass `name` here, the result must stay anonymous so it can be merged with the
|
|
268
|
+
// adjacent synthetic object in `mergeAdjacentObjectsLazy`. Nested enum qualification
|
|
269
|
+
// happens upstream via `convertObject`'s `setEnumName` propagation.
|
|
220
270
|
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
|
|
221
271
|
}
|
|
222
272
|
|
|
223
273
|
for (const { propertyName, value } of filteredDiscriminantValues) {
|
|
224
|
-
allOfMembers.push(
|
|
274
|
+
allOfMembers.push(createDiscriminantNode({ propertyName, value }))
|
|
225
275
|
}
|
|
226
276
|
|
|
227
|
-
return ast.createSchema({
|
|
277
|
+
return ast.factory.createSchema({
|
|
228
278
|
type: 'intersection',
|
|
229
|
-
members: [...ast.
|
|
279
|
+
members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
|
|
230
280
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
231
281
|
})
|
|
232
282
|
}
|
|
@@ -243,7 +293,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
243
293
|
return null
|
|
244
294
|
}
|
|
245
295
|
|
|
246
|
-
return ast.createSchema({
|
|
296
|
+
return ast.factory.createSchema({
|
|
247
297
|
type: 'object',
|
|
248
298
|
primitive: 'object',
|
|
249
299
|
properties: [discriminatorProperty],
|
|
@@ -254,10 +304,10 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
254
304
|
const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
|
|
255
305
|
const unionBase = {
|
|
256
306
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
257
|
-
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
307
|
+
discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
258
308
|
strategy,
|
|
259
309
|
}
|
|
260
|
-
const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
|
|
310
|
+
const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
|
|
261
311
|
const sharedPropertiesNode = schema.properties
|
|
262
312
|
? (() => {
|
|
263
313
|
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
@@ -270,9 +320,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
270
320
|
|
|
271
321
|
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
272
322
|
const members = unionMembers.map((s) => {
|
|
273
|
-
const ref = isReference(s) ? s.$ref : undefined
|
|
274
|
-
const discriminatorValue =
|
|
275
|
-
const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
|
|
323
|
+
const ref = dialect.isReference(s) ? s.$ref : undefined
|
|
324
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
|
|
325
|
+
const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
|
|
276
326
|
|
|
277
327
|
if (!discriminatorValue || !discriminator) {
|
|
278
328
|
return memberNode
|
|
@@ -289,12 +339,12 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
289
339
|
)
|
|
290
340
|
: undefined
|
|
291
341
|
|
|
292
|
-
return ast.createSchema({
|
|
342
|
+
return ast.factory.createSchema({
|
|
293
343
|
type: 'intersection',
|
|
294
344
|
members: [
|
|
295
345
|
memberNode,
|
|
296
346
|
narrowedDiscriminatorNode ??
|
|
297
|
-
|
|
347
|
+
createDiscriminantNode({
|
|
298
348
|
propertyName: discriminator.propertyName,
|
|
299
349
|
value: discriminatorValue,
|
|
300
350
|
}),
|
|
@@ -302,7 +352,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
302
352
|
})
|
|
303
353
|
})
|
|
304
354
|
|
|
305
|
-
const unionNode = ast.createSchema({
|
|
355
|
+
const unionNode = ast.factory.createSchema({
|
|
306
356
|
type: 'union',
|
|
307
357
|
...unionBase,
|
|
308
358
|
members,
|
|
@@ -312,17 +362,17 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
312
362
|
return unionNode
|
|
313
363
|
}
|
|
314
364
|
|
|
315
|
-
return ast.createSchema({
|
|
365
|
+
return ast.factory.createSchema({
|
|
316
366
|
type: 'intersection',
|
|
317
367
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
318
368
|
members: [unionNode, sharedPropertiesNode],
|
|
319
369
|
})
|
|
320
370
|
}
|
|
321
371
|
|
|
322
|
-
return ast.createSchema({
|
|
372
|
+
return ast.factory.createSchema({
|
|
323
373
|
type: 'union',
|
|
324
374
|
...unionBase,
|
|
325
|
-
members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
|
|
375
|
+
members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))),
|
|
326
376
|
})
|
|
327
377
|
}
|
|
328
378
|
|
|
@@ -333,18 +383,19 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
333
383
|
const constValue = schema.const
|
|
334
384
|
|
|
335
385
|
if (constValue === null) {
|
|
336
|
-
return ast.createSchema({
|
|
386
|
+
return ast.factory.createSchema({
|
|
337
387
|
type: 'null',
|
|
338
388
|
primitive: 'null',
|
|
339
389
|
name,
|
|
340
390
|
title: schema.title,
|
|
341
391
|
description: schema.description,
|
|
342
392
|
deprecated: schema.deprecated,
|
|
393
|
+
format: schema.format,
|
|
343
394
|
})
|
|
344
395
|
}
|
|
345
396
|
|
|
346
397
|
const constPrimitive = getPrimitiveType(typeof constValue === 'number' ? 'number' : typeof constValue === 'boolean' ? 'boolean' : 'string')
|
|
347
|
-
return ast.createSchema({
|
|
398
|
+
return ast.factory.createSchema({
|
|
348
399
|
type: 'enum',
|
|
349
400
|
primitive: constPrimitive,
|
|
350
401
|
enumValues: [constValue as string | number | boolean],
|
|
@@ -360,7 +411,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
360
411
|
const base = buildSchemaNode(schema, name, nullable, defaultValue)
|
|
361
412
|
|
|
362
413
|
if (schema.format === 'int64') {
|
|
363
|
-
return ast.createSchema({
|
|
414
|
+
return ast.factory.createSchema({
|
|
364
415
|
type: options.integerType === 'bigint' ? 'bigint' : 'integer',
|
|
365
416
|
primitive: 'integer',
|
|
366
417
|
...base,
|
|
@@ -376,7 +427,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
376
427
|
if (!dateType) return null
|
|
377
428
|
|
|
378
429
|
if (dateType.type === 'datetime') {
|
|
379
|
-
return ast.createSchema({
|
|
430
|
+
return ast.factory.createSchema({
|
|
380
431
|
...base,
|
|
381
432
|
primitive: 'string' as const,
|
|
382
433
|
type: 'datetime',
|
|
@@ -384,7 +435,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
384
435
|
local: dateType.local,
|
|
385
436
|
})
|
|
386
437
|
}
|
|
387
|
-
return ast.createSchema({
|
|
438
|
+
return ast.factory.createSchema({
|
|
388
439
|
...base,
|
|
389
440
|
primitive: 'string' as const,
|
|
390
441
|
type: dateType.type,
|
|
@@ -398,14 +449,14 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
398
449
|
const specialPrimitive: ast.PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
|
|
399
450
|
|
|
400
451
|
if (specialType === 'number' || specialType === 'integer' || specialType === 'bigint') {
|
|
401
|
-
return ast.createSchema({
|
|
452
|
+
return ast.factory.createSchema({
|
|
402
453
|
...base,
|
|
403
454
|
primitive: specialPrimitive,
|
|
404
455
|
type: specialType,
|
|
405
456
|
})
|
|
406
457
|
}
|
|
407
458
|
if (specialType === 'url') {
|
|
408
|
-
return ast.createSchema({
|
|
459
|
+
return ast.factory.createSchema({
|
|
409
460
|
...base,
|
|
410
461
|
primitive: 'string' as const,
|
|
411
462
|
type: 'url',
|
|
@@ -414,21 +465,21 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
414
465
|
})
|
|
415
466
|
}
|
|
416
467
|
if (specialType === 'ipv4') {
|
|
417
|
-
return ast.createSchema({
|
|
468
|
+
return ast.factory.createSchema({
|
|
418
469
|
...base,
|
|
419
470
|
primitive: 'string' as const,
|
|
420
471
|
type: 'ipv4',
|
|
421
472
|
})
|
|
422
473
|
}
|
|
423
474
|
if (specialType === 'ipv6') {
|
|
424
|
-
return ast.createSchema({
|
|
475
|
+
return ast.factory.createSchema({
|
|
425
476
|
...base,
|
|
426
477
|
primitive: 'string' as const,
|
|
427
478
|
type: 'ipv6',
|
|
428
479
|
})
|
|
429
480
|
}
|
|
430
481
|
if (specialType === 'uuid' || specialType === 'email') {
|
|
431
|
-
return ast.createSchema({
|
|
482
|
+
return ast.factory.createSchema({
|
|
432
483
|
...base,
|
|
433
484
|
primitive: 'string' as const,
|
|
434
485
|
type: specialType,
|
|
@@ -437,7 +488,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
437
488
|
})
|
|
438
489
|
}
|
|
439
490
|
|
|
440
|
-
return ast.createSchema({
|
|
491
|
+
return ast.factory.createSchema({
|
|
441
492
|
...base,
|
|
442
493
|
primitive: specialPrimitive,
|
|
443
494
|
type: specialType as ast.ScalarSchemaType,
|
|
@@ -454,6 +505,22 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
454
505
|
|
|
455
506
|
const nullInEnum = schema.enum!.includes(null)
|
|
456
507
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
508
|
+
|
|
509
|
+
// drf-spectacular `NullEnum` ({ enum: [null] }) is just `null`. An empty enum node would
|
|
510
|
+
// render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
|
|
511
|
+
// branch so it renders as a clean `null` (not `z.null().nullable()`).
|
|
512
|
+
if (nullInEnum && filteredValues.length === 0) {
|
|
513
|
+
return ast.factory.createSchema({
|
|
514
|
+
type: 'null',
|
|
515
|
+
primitive: 'null',
|
|
516
|
+
name,
|
|
517
|
+
title: schema.title,
|
|
518
|
+
description: schema.description,
|
|
519
|
+
deprecated: schema.deprecated,
|
|
520
|
+
format: schema.format,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
457
524
|
const enumNullable = nullable || nullInEnum || undefined
|
|
458
525
|
const enumDefault = schema.default === null && enumNullable ? undefined : schema.default
|
|
459
526
|
const enumPrimitive = getPrimitiveType(type)
|
|
@@ -470,6 +537,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
470
537
|
writeOnly: schema.writeOnly,
|
|
471
538
|
default: enumDefault,
|
|
472
539
|
example: schema.example,
|
|
540
|
+
format: schema.format,
|
|
473
541
|
}
|
|
474
542
|
|
|
475
543
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
@@ -482,7 +550,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
482
550
|
const uniqueValues = [...new Set(filteredValues)]
|
|
483
551
|
const seenNames = new Set<string>()
|
|
484
552
|
|
|
485
|
-
return ast.createSchema({
|
|
553
|
+
return ast.factory.createSchema({
|
|
486
554
|
...enumBase,
|
|
487
555
|
primitive: enumPrimitiveType,
|
|
488
556
|
namedEnumValues: uniqueValues
|
|
@@ -499,7 +567,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
499
567
|
})
|
|
500
568
|
}
|
|
501
569
|
|
|
502
|
-
return ast.createSchema({
|
|
570
|
+
return ast.factory.createSchema({
|
|
503
571
|
...enumBase,
|
|
504
572
|
enumValues: [...new Set(filteredValues)],
|
|
505
573
|
})
|
|
@@ -513,21 +581,23 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
513
581
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
514
582
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
515
583
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
516
|
-
const propNullable = isNullable(resolvedPropSchema)
|
|
584
|
+
const propNullable = dialect.isNullable(resolvedPropSchema)
|
|
517
585
|
|
|
518
|
-
const resolvedChildName =
|
|
586
|
+
const resolvedChildName = childName(name, propName)
|
|
519
587
|
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
588
|
+
const schemaNode = (() => {
|
|
589
|
+
const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
|
|
590
|
+
const tupleNode = ast.narrowSchema(node, 'tuple')
|
|
591
|
+
if (tupleNode?.items) {
|
|
592
|
+
const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
|
|
593
|
+
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
594
|
+
return { ...tupleNode, items: namedItems }
|
|
595
|
+
}
|
|
527
596
|
}
|
|
528
|
-
|
|
597
|
+
return node
|
|
598
|
+
})()
|
|
529
599
|
|
|
530
|
-
return ast.createProperty({
|
|
600
|
+
return ast.factory.createProperty({
|
|
531
601
|
name: propName,
|
|
532
602
|
schema: {
|
|
533
603
|
...schemaNode,
|
|
@@ -539,18 +609,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
539
609
|
: []
|
|
540
610
|
|
|
541
611
|
const additionalProperties = schema.additionalProperties
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
type: typeOptionMap.get(options.unknownType)!,
|
|
552
|
-
})
|
|
553
|
-
}
|
|
612
|
+
const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
|
|
613
|
+
if (additionalProperties === true) return true
|
|
614
|
+
if (additionalProperties === false) return false
|
|
615
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
616
|
+
return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
617
|
+
}
|
|
618
|
+
if (additionalProperties) return ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
619
|
+
return undefined
|
|
620
|
+
})()
|
|
554
621
|
|
|
555
622
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
556
623
|
|
|
@@ -559,7 +626,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
559
626
|
Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
|
|
560
627
|
pattern,
|
|
561
628
|
patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
|
|
562
|
-
? ast.createSchema({
|
|
629
|
+
? ast.factory.createSchema({
|
|
563
630
|
type: typeOptionMap.get(options.unknownType)!,
|
|
564
631
|
})
|
|
565
632
|
: parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
|
|
@@ -567,7 +634,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
567
634
|
)
|
|
568
635
|
: undefined
|
|
569
636
|
|
|
570
|
-
const objectNode: ast.SchemaNode = ast.createSchema({
|
|
637
|
+
const objectNode: ast.SchemaNode = ast.factory.createSchema({
|
|
571
638
|
type: 'object',
|
|
572
639
|
primitive: 'object',
|
|
573
640
|
properties,
|
|
@@ -578,10 +645,10 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
578
645
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
579
646
|
})
|
|
580
647
|
|
|
581
|
-
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
648
|
+
if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
582
649
|
const discPropName = schema.discriminator.propertyName
|
|
583
650
|
const values = Object.keys(schema.discriminator.mapping)
|
|
584
|
-
const enumName = name ?
|
|
651
|
+
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
|
|
585
652
|
return ast.setDiscriminatorEnum({
|
|
586
653
|
node: objectNode,
|
|
587
654
|
propertyName: discPropName,
|
|
@@ -598,9 +665,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
598
665
|
*/
|
|
599
666
|
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
600
667
|
const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
|
|
601
|
-
const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : ast.createSchema({ type: 'any' })
|
|
668
|
+
const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : ast.factory.createSchema({ type: 'any' })
|
|
602
669
|
|
|
603
|
-
return ast.createSchema({
|
|
670
|
+
return ast.factory.createSchema({
|
|
604
671
|
type: 'tuple',
|
|
605
672
|
primitive: 'array',
|
|
606
673
|
items: tupleItems,
|
|
@@ -616,10 +683,10 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
616
683
|
*/
|
|
617
684
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
618
685
|
const rawItems = schema.items as SchemaObject | undefined
|
|
619
|
-
const itemName = rawItems?.enum?.length && name ?
|
|
686
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name
|
|
620
687
|
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
621
688
|
|
|
622
|
-
return ast.createSchema({
|
|
689
|
+
return ast.factory.createSchema({
|
|
623
690
|
type: 'array',
|
|
624
691
|
primitive: 'array',
|
|
625
692
|
items,
|
|
@@ -634,7 +701,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
634
701
|
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
635
702
|
*/
|
|
636
703
|
function convertString({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
637
|
-
return ast.createSchema({
|
|
704
|
+
return ast.factory.createSchema({
|
|
638
705
|
type: 'string',
|
|
639
706
|
primitive: 'string',
|
|
640
707
|
min: schema.minLength,
|
|
@@ -648,7 +715,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
648
715
|
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
649
716
|
*/
|
|
650
717
|
function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): ast.SchemaNode {
|
|
651
|
-
return ast.createSchema({
|
|
718
|
+
return ast.factory.createSchema({
|
|
652
719
|
type,
|
|
653
720
|
primitive: type,
|
|
654
721
|
min: schema.minimum,
|
|
@@ -664,7 +731,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
664
731
|
* Converts a `type: 'boolean'` schema.
|
|
665
732
|
*/
|
|
666
733
|
function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
667
|
-
return ast.createSchema({
|
|
734
|
+
return ast.factory.createSchema({
|
|
668
735
|
type: 'boolean',
|
|
669
736
|
primitive: 'boolean',
|
|
670
737
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
@@ -675,7 +742,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
675
742
|
* Converts an explicit `type: 'null'` schema.
|
|
676
743
|
*/
|
|
677
744
|
function convertNull({ schema, name, nullable }: SchemaContext): ast.SchemaNode {
|
|
678
|
-
return ast.createSchema({
|
|
745
|
+
return ast.factory.createSchema({
|
|
679
746
|
type: 'null',
|
|
680
747
|
primitive: 'null',
|
|
681
748
|
name,
|
|
@@ -683,15 +750,90 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
683
750
|
description: schema.description,
|
|
684
751
|
deprecated: schema.deprecated,
|
|
685
752
|
nullable,
|
|
753
|
+
format: schema.format,
|
|
686
754
|
})
|
|
687
755
|
}
|
|
688
756
|
|
|
689
757
|
/**
|
|
690
|
-
*
|
|
758
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
759
|
+
* into a `blob` node.
|
|
760
|
+
*/
|
|
761
|
+
function convertBlob({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
762
|
+
return ast.factory.createSchema({
|
|
763
|
+
type: 'blob',
|
|
764
|
+
primitive: 'string',
|
|
765
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
691
771
|
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
|
|
772
|
+
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
|
|
773
|
+
* falls through and handles it as that single type with nullability already folded in.
|
|
774
|
+
*/
|
|
775
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode | null {
|
|
776
|
+
const types = schema.type as Array<string>
|
|
777
|
+
const nonNullTypes = types.filter((t) => t !== 'null')
|
|
778
|
+
if (nonNullTypes.length <= 1) return null
|
|
779
|
+
|
|
780
|
+
const arrayNullable = types.includes('null') || nullable || undefined
|
|
781
|
+
return ast.factory.createSchema({
|
|
782
|
+
type: 'union',
|
|
783
|
+
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
784
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
790
|
+
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
791
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
792
|
+
* match/convert/fall-through contract.
|
|
793
|
+
*/
|
|
794
|
+
const schemaRules: Array<SchemaRule> = [
|
|
795
|
+
{ name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
|
|
796
|
+
{ name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
|
|
797
|
+
{ name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
|
|
798
|
+
{ name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
|
|
799
|
+
{ name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
|
|
800
|
+
{
|
|
801
|
+
name: 'blob',
|
|
802
|
+
match: ({ schema }) => dialect.isBinary(schema),
|
|
803
|
+
convert: convertBlob,
|
|
804
|
+
},
|
|
805
|
+
{ name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
|
|
806
|
+
{
|
|
807
|
+
name: 'constrained-string',
|
|
808
|
+
match: ({ schema, type }) => !type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined),
|
|
809
|
+
convert: convertString,
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: 'constrained-number',
|
|
813
|
+
match: ({ schema, type }) => !type && (schema.minimum !== undefined || schema.maximum !== undefined),
|
|
814
|
+
convert: (ctx) => convertNumeric(ctx, 'number'),
|
|
815
|
+
},
|
|
816
|
+
{ name: 'enum', match: ({ schema }) => !!schema.enum?.length, convert: convertEnum },
|
|
817
|
+
{
|
|
818
|
+
name: 'object',
|
|
819
|
+
match: ({ schema, type }) => type === 'object' || !!schema.properties || !!schema.additionalProperties || 'patternProperties' in schema,
|
|
820
|
+
convert: convertObject,
|
|
821
|
+
},
|
|
822
|
+
{ name: 'tuple', match: ({ schema }) => 'prefixItems' in schema, convert: convertTuple },
|
|
823
|
+
{ name: 'array', match: ({ schema, type }) => type === 'array' || 'items' in schema, convert: convertArray },
|
|
824
|
+
{ name: 'string', match: ({ type }) => type === 'string', convert: convertString },
|
|
825
|
+
{ name: 'number', match: ({ type }) => type === 'number', convert: (ctx) => convertNumeric(ctx, 'number') },
|
|
826
|
+
{ name: 'integer', match: ({ type }) => type === 'integer', convert: (ctx) => convertNumeric(ctx, 'integer') },
|
|
827
|
+
{ name: 'boolean', match: ({ type }) => type === 'boolean', convert: convertBoolean },
|
|
828
|
+
{ name: 'null', match: ({ type }) => type === 'null', convert: convertNull },
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
833
|
+
*
|
|
834
|
+
* Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
|
|
835
|
+
* the first converter that produces a node. When none match, falls back to the configured
|
|
836
|
+
* `emptySchemaType`.
|
|
695
837
|
*/
|
|
696
838
|
function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
|
|
697
839
|
const options: ast.ParserOptions = {
|
|
@@ -703,11 +845,11 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
703
845
|
return parseSchema({ schema: flattenedSchema, name }, rawOptions)
|
|
704
846
|
}
|
|
705
847
|
|
|
706
|
-
const nullable = isNullable(schema) || undefined
|
|
848
|
+
const nullable = dialect.isNullable(schema) || undefined
|
|
707
849
|
const defaultValue = schema.default === null && nullable ? undefined : schema.default
|
|
708
850
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
709
851
|
|
|
710
|
-
const
|
|
852
|
+
const schemaCtx: SchemaContext = {
|
|
711
853
|
schema,
|
|
712
854
|
name,
|
|
713
855
|
nullable,
|
|
@@ -717,80 +859,36 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
717
859
|
options,
|
|
718
860
|
}
|
|
719
861
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (unionMembers.length) return convertUnion(ctx)
|
|
725
|
-
|
|
726
|
-
if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
|
|
727
|
-
|
|
728
|
-
if (schema.format) {
|
|
729
|
-
const formatResult = convertFormat(ctx)
|
|
730
|
-
if (formatResult) return formatResult
|
|
862
|
+
for (const rule of schemaRules) {
|
|
863
|
+
if (!rule.match(schemaCtx)) continue
|
|
864
|
+
const node = rule.convert(schemaCtx)
|
|
865
|
+
if (node) return node
|
|
731
866
|
}
|
|
732
867
|
|
|
733
|
-
if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
|
|
734
|
-
return ast.createSchema({
|
|
735
|
-
type: 'blob',
|
|
736
|
-
primitive: 'string',
|
|
737
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
738
|
-
})
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
742
|
-
const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
|
|
743
|
-
const arrayNullable = schema.type.includes('null') || nullable || undefined
|
|
744
|
-
|
|
745
|
-
if (nonNullTypes.length > 1) {
|
|
746
|
-
return ast.createSchema({
|
|
747
|
-
type: 'union',
|
|
748
|
-
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
749
|
-
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
750
|
-
})
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (!type) {
|
|
755
|
-
if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
|
|
756
|
-
return convertString(ctx)
|
|
757
|
-
}
|
|
758
|
-
if (schema.minimum !== undefined || schema.maximum !== undefined) {
|
|
759
|
-
return convertNumeric(ctx, 'number')
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (schema.enum?.length) return convertEnum(ctx)
|
|
764
|
-
if (type === 'object' || schema.properties || schema.additionalProperties || 'patternProperties' in schema) return convertObject(ctx)
|
|
765
|
-
if ('prefixItems' in schema) return convertTuple(ctx)
|
|
766
|
-
if (type === 'array' || 'items' in schema) return convertArray(ctx)
|
|
767
|
-
if (type === 'string') return convertString(ctx)
|
|
768
|
-
if (type === 'number') return convertNumeric(ctx, 'number')
|
|
769
|
-
if (type === 'integer') return convertNumeric(ctx, 'integer')
|
|
770
|
-
if (type === 'boolean') return convertBoolean(ctx)
|
|
771
|
-
if (type === 'null') return convertNull(ctx)
|
|
772
|
-
|
|
773
868
|
const emptyType = typeOptionMap.get(options.emptySchemaType)!
|
|
774
|
-
return ast.createSchema({
|
|
869
|
+
return ast.factory.createSchema({
|
|
775
870
|
type: emptyType as ast.ScalarSchemaType,
|
|
776
871
|
name,
|
|
777
872
|
title: schema.title,
|
|
778
873
|
description: schema.description,
|
|
874
|
+
format: schema.format,
|
|
779
875
|
})
|
|
780
876
|
}
|
|
781
877
|
|
|
782
878
|
/**
|
|
783
879
|
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
784
880
|
*/
|
|
785
|
-
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown
|
|
881
|
+
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>, parentName?: string): ast.ParameterNode {
|
|
786
882
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
883
|
+
const paramName = param['name'] as string
|
|
884
|
+
const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : undefined
|
|
787
885
|
|
|
788
886
|
const schema: ast.SchemaNode = param['schema']
|
|
789
|
-
? parseSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
790
|
-
: ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
887
|
+
? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
|
|
888
|
+
: ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
791
889
|
|
|
792
|
-
return ast.createParameter({
|
|
793
|
-
name:
|
|
890
|
+
return ast.factory.createParameter({
|
|
891
|
+
name: paramName,
|
|
794
892
|
in: param['in'] as ast.ParameterLocation,
|
|
795
893
|
schema: {
|
|
796
894
|
...schema,
|
|
@@ -839,25 +937,27 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
839
937
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
840
938
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
841
939
|
*/
|
|
842
|
-
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string
|
|
843
|
-
if (!schema?.properties) return
|
|
940
|
+
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): Array<string> | null {
|
|
941
|
+
if (!schema?.properties) return null
|
|
844
942
|
|
|
845
|
-
const keys: string
|
|
943
|
+
const keys: Array<string> = []
|
|
846
944
|
for (const key in schema.properties) {
|
|
847
945
|
const prop = schema.properties[key]
|
|
848
|
-
if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
946
|
+
if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
849
947
|
keys.push(key)
|
|
850
948
|
}
|
|
851
949
|
}
|
|
852
|
-
return keys.length ? keys :
|
|
950
|
+
return keys.length ? keys : null
|
|
853
951
|
}
|
|
854
952
|
|
|
855
953
|
/**
|
|
856
954
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
857
955
|
*/
|
|
858
956
|
function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
|
|
957
|
+
const operationId = getOperationId(operation)
|
|
958
|
+
const operationName = operationId ? pascalCase(operationId) : undefined
|
|
859
959
|
const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
|
|
860
|
-
parseParameter(options, param as unknown as Record<string, unknown
|
|
960
|
+
parseParameter(options, param as unknown as Record<string, unknown>, operationName),
|
|
861
961
|
)
|
|
862
962
|
|
|
863
963
|
// Determine which content types to include in requestBody.content.
|
|
@@ -866,6 +966,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
866
966
|
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation)
|
|
867
967
|
|
|
868
968
|
const requestBodyMeta = getRequestBodyMeta(operation)
|
|
969
|
+
const requestBodyName = operationName ? `${operationName}Request` : undefined
|
|
869
970
|
|
|
870
971
|
const content = allContentTypes.flatMap((ct) => {
|
|
871
972
|
const schema = getRequestSchema(document, operation, { contentType: ct })
|
|
@@ -873,7 +974,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
873
974
|
return [
|
|
874
975
|
{
|
|
875
976
|
contentType: ct,
|
|
876
|
-
schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
|
|
977
|
+
schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
|
|
877
978
|
keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
|
|
878
979
|
},
|
|
879
980
|
]
|
|
@@ -888,39 +989,60 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
888
989
|
}
|
|
889
990
|
: undefined
|
|
890
991
|
|
|
891
|
-
const responses: Array<ast.ResponseNode> =
|
|
892
|
-
const responseObj =
|
|
893
|
-
|
|
992
|
+
const responses: Array<ast.ResponseNode> = getResponseStatusCodes(operation).map((statusCode) => {
|
|
993
|
+
const responseObj = getResponseByStatusCode({ document, operation, statusCode })
|
|
994
|
+
|
|
995
|
+
// Use `Status<code>` (matching plugin-ts's resolveResponseStatusName convention) so the
|
|
996
|
+
// qualified names for nested enums don't collide with top-level component schemas that
|
|
997
|
+
// happen to be named `<operation><statusCode>` (e.g. `GetMaintenance200`).
|
|
998
|
+
const responseName = operationName ? `${operationName}Status${statusCode}` : undefined
|
|
999
|
+
const { description } = getResponseMeta(responseObj)
|
|
1000
|
+
|
|
1001
|
+
const parseEntrySchema = (contentType?: string) => {
|
|
1002
|
+
const raw = getResponseSchema(document, operation, statusCode, { contentType })
|
|
1003
|
+
const node =
|
|
1004
|
+
raw && Object.keys(raw).length > 0
|
|
1005
|
+
? parseSchema({ schema: raw, name: responseName }, options)
|
|
1006
|
+
: ast.factory.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
1007
|
+
return { schema: node, keysToOmit: collectPropertyKeysByFlag(raw, 'writeOnly') }
|
|
1008
|
+
}
|
|
894
1009
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
type: typeOptionMap.get(options.emptySchemaType)!,
|
|
900
|
-
})
|
|
1010
|
+
// Build one entry per declared response content type so plugins can union the variants.
|
|
1011
|
+
// When a global contentType is configured, restrict to that single type (mirrors requestBody).
|
|
1012
|
+
const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
|
|
1013
|
+
const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
|
|
901
1014
|
|
|
902
|
-
|
|
903
|
-
|
|
1015
|
+
// Body-less responses keep a single fallback entry so the response still resolves to a
|
|
1016
|
+
// (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
|
|
1017
|
+
if (content.length === 0) {
|
|
1018
|
+
content.push({ contentType: getRequestContentType({ document, operation }) || 'application/json', ...parseEntrySchema(ctx.contentType) })
|
|
1019
|
+
}
|
|
904
1020
|
|
|
905
|
-
return ast.createResponse({
|
|
1021
|
+
return ast.factory.createResponse({
|
|
906
1022
|
statusCode: statusCode as ast.StatusCode,
|
|
907
1023
|
description,
|
|
908
|
-
|
|
909
|
-
mediaType,
|
|
910
|
-
keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
|
|
1024
|
+
content,
|
|
911
1025
|
})
|
|
912
1026
|
})
|
|
913
1027
|
|
|
914
|
-
const
|
|
1028
|
+
const pathItem = document.paths?.[operation.path]
|
|
1029
|
+
const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? (pathItem as { summary?: unknown; description?: unknown }) : undefined
|
|
1030
|
+
const pickDoc = (key: 'summary' | 'description'): string | undefined => {
|
|
1031
|
+
const own = operation.schema[key]
|
|
1032
|
+
if (typeof own === 'string') return own
|
|
1033
|
+
const fallback = pathItemDoc?.[key]
|
|
1034
|
+
return typeof fallback === 'string' ? fallback : undefined
|
|
1035
|
+
}
|
|
915
1036
|
|
|
916
|
-
return ast.createOperation({
|
|
917
|
-
operationId
|
|
1037
|
+
return ast.factory.createOperation({
|
|
1038
|
+
operationId,
|
|
1039
|
+
protocol: 'http',
|
|
918
1040
|
method: operation.method.toUpperCase() as ast.HttpMethod,
|
|
919
|
-
path:
|
|
920
|
-
tags: operation.
|
|
921
|
-
summary:
|
|
922
|
-
description:
|
|
923
|
-
deprecated: operation.
|
|
1041
|
+
path: operation.path,
|
|
1042
|
+
tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
|
|
1043
|
+
summary: pickDoc('summary') || undefined,
|
|
1044
|
+
description: pickDoc('description') || undefined,
|
|
1045
|
+
deprecated: operation.schema.deprecated || undefined,
|
|
924
1046
|
parameters,
|
|
925
1047
|
requestBody,
|
|
926
1048
|
responses,
|
|
@@ -933,10 +1055,11 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
933
1055
|
/**
|
|
934
1056
|
* Parses a single OpenAPI `SchemaObject` into a `SchemaNode`.
|
|
935
1057
|
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
1058
|
+
* Reach for this when you only need one schema, not the whole spec. To parse a full spec
|
|
1059
|
+
* with its operations and schemas, call `parseOas()`.
|
|
938
1060
|
*
|
|
939
|
-
* @note
|
|
1061
|
+
* @note Internal state tracks `$ref` paths under resolution, so circular schemas stop
|
|
1062
|
+
* recursing instead of looping.
|
|
940
1063
|
*
|
|
941
1064
|
* @example
|
|
942
1065
|
* ```ts
|
|
@@ -957,8 +1080,8 @@ export function parseSchema(
|
|
|
957
1080
|
* Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
|
|
958
1081
|
*
|
|
959
1082
|
* This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
|
|
960
|
-
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here
|
|
961
|
-
*
|
|
1083
|
+
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here.
|
|
1084
|
+
* The tree is a pure data structure of all schemas and operations.
|
|
962
1085
|
*
|
|
963
1086
|
* Returns the AST root and a `nameMapping` for resolving schema references.
|
|
964
1087
|
*
|
|
@@ -987,16 +1110,11 @@ export function parseOas(
|
|
|
987
1110
|
|
|
988
1111
|
const schemas: Array<ast.SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
|
|
989
1112
|
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
|
|
994
|
-
Object.entries(methods)
|
|
995
|
-
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
996
|
-
.filter((op): op is ast.OperationNode => op !== null),
|
|
997
|
-
)
|
|
1113
|
+
const operations: Array<ast.OperationNode> = getOperations(document)
|
|
1114
|
+
.map((operation) => _parseOperation(mergedOptions, operation))
|
|
1115
|
+
.filter((op): op is ast.OperationNode => op !== null)
|
|
998
1116
|
|
|
999
|
-
const root = ast.createInput({ schemas, operations })
|
|
1117
|
+
const root = ast.factory.createInput({ schemas, operations })
|
|
1000
1118
|
|
|
1001
1119
|
return { root, nameMapping }
|
|
1002
1120
|
}
|