@kubb/adapter-oas 5.0.0-beta.4 → 5.0.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/index.cjs +900 -311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +52 -69
- package/dist/index.js +902 -310
- package/dist/index.js.map +1 -1
- package/extension.yaml +144 -57
- package/package.json +6 -4
- package/src/adapter.bench.ts +64 -0
- package/src/adapter.ts +143 -48
- package/src/constants.ts +8 -0
- package/src/dialect.ts +38 -0
- package/src/discriminator.ts +31 -47
- package/src/factory.ts +38 -12
- package/src/index.ts +2 -2
- package/src/parser.ts +233 -137
- package/src/refs.ts +31 -5
- package/src/resolvers.ts +77 -38
- package/src/schemaDiagnostics.ts +76 -0
- package/src/stream.ts +278 -0
- package/src/types.ts +34 -11
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/parser.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { URLPath } from '@internals/utils'
|
|
1
|
+
import { pascalCase, URLPath } from '@internals/utils'
|
|
2
2
|
import { ast } from '@kubb/core'
|
|
3
3
|
import BaseOas from 'oas'
|
|
4
4
|
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, SCHEMA_REF_PREFIX, typeOptionMap } from './constants.ts'
|
|
5
|
-
import {
|
|
6
|
-
import { resolveRef } from './refs.ts'
|
|
5
|
+
import { oasDialect, type OasDialect } from './dialect.ts'
|
|
7
6
|
import {
|
|
8
7
|
buildSchemaNode,
|
|
9
8
|
flattenSchema,
|
|
10
9
|
getDateType,
|
|
11
|
-
getMediaType,
|
|
12
10
|
getParameters,
|
|
13
11
|
getPrimitiveType,
|
|
14
12
|
getRequestBodyContentTypes,
|
|
15
13
|
getRequestSchema,
|
|
14
|
+
getResponseBodyContentTypes,
|
|
16
15
|
getResponseSchema,
|
|
17
16
|
getSchemas,
|
|
18
17
|
getSchemaType,
|
|
@@ -30,6 +29,16 @@ export type OasParserContext = {
|
|
|
30
29
|
contentType?: ContentType
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The object returned by {@link createSchemaParser}.
|
|
34
|
+
* Contains parser functions bound to a specific document.
|
|
35
|
+
*/
|
|
36
|
+
export type SchemaParser = {
|
|
37
|
+
parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
|
|
38
|
+
parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
|
|
39
|
+
parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/**
|
|
34
43
|
* Pre-computed per-schema context passed to every schema converter.
|
|
35
44
|
*
|
|
@@ -50,6 +59,13 @@ type SchemaContext = {
|
|
|
50
59
|
options: ast.ParserOptions
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* One entry in this adapter's schema-dispatch table, a {@link ast.DispatchRule} specialized
|
|
64
|
+
* to OAS schema context and Kubb `SchemaNode` output. See {@link ast.dispatch} for the contract
|
|
65
|
+
* a future adapter (e.g. AsyncAPI) follows: define a context type and an ordered rules table.
|
|
66
|
+
*/
|
|
67
|
+
type SchemaRule = ast.DispatchRule<SchemaContext, ast.SchemaNode>
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
55
71
|
*
|
|
@@ -76,12 +92,12 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
|
76
92
|
* Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
|
|
77
93
|
* made possible by hoisting of function declarations.
|
|
78
94
|
*
|
|
79
|
-
* @
|
|
95
|
+
* @internal
|
|
80
96
|
*/
|
|
81
|
-
function createSchemaParser(ctx: OasParserContext) {
|
|
97
|
+
export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
|
|
82
98
|
const document = ctx.document
|
|
83
99
|
|
|
84
|
-
// Branch handlers
|
|
100
|
+
// Branch handlers, each converts one OAS schema pattern to a SchemaNode.
|
|
85
101
|
|
|
86
102
|
/**
|
|
87
103
|
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
@@ -89,6 +105,16 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
89
105
|
*/
|
|
90
106
|
const resolvingRefs = new Set<string>()
|
|
91
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
110
|
+
*
|
|
111
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
112
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
113
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
114
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
115
|
+
*/
|
|
116
|
+
const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
94
120
|
*
|
|
@@ -98,19 +124,23 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
98
124
|
* Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
|
|
99
125
|
*/
|
|
100
126
|
function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
101
|
-
let resolvedSchema: ast.SchemaNode |
|
|
127
|
+
let resolvedSchema: ast.SchemaNode | null = null
|
|
102
128
|
const refPath = schema.$ref
|
|
103
129
|
if (refPath && !resolvingRefs.has(refPath)) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
if (!resolvedRefCache.has(refPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
|
|
133
|
+
if (referenced) {
|
|
134
|
+
resolvingRefs.add(refPath)
|
|
135
|
+
resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
|
|
136
|
+
resolvingRefs.delete(refPath)
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
141
|
+
resolvedRefCache.set(refPath, resolvedSchema)
|
|
113
142
|
}
|
|
143
|
+
resolvedSchema = resolvedRefCache.get(refPath) ?? null
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
return ast.createSchema({
|
|
@@ -133,7 +163,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
133
163
|
schema.additionalProperties === undefined
|
|
134
164
|
) {
|
|
135
165
|
const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
|
|
136
|
-
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name
|
|
166
|
+
const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name }, rawOptions)
|
|
137
167
|
const { kind: _kind, ...memberNodeProps } = memberNode
|
|
138
168
|
const mergedNullable = nullable || memberNode.nullable || undefined
|
|
139
169
|
const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
|
|
@@ -150,6 +180,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
150
180
|
default: mergedDefault,
|
|
151
181
|
example: schema.example ?? memberNode.example,
|
|
152
182
|
pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
|
|
183
|
+
format: schema.format ?? memberNode.format,
|
|
153
184
|
} as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
|
|
154
185
|
}
|
|
155
186
|
|
|
@@ -159,13 +190,13 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
159
190
|
}> = []
|
|
160
191
|
const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
161
192
|
.filter((item) => {
|
|
162
|
-
if (!isReference(item) || !name) return true
|
|
163
|
-
const deref = resolveRef<SchemaObject>(document, item.$ref)
|
|
164
|
-
if (!deref || !isDiscriminator(deref)) return true
|
|
193
|
+
if (!dialect.isReference(item) || !name) return true
|
|
194
|
+
const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
|
|
195
|
+
if (!deref || !dialect.isDiscriminator(deref)) return true
|
|
165
196
|
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
166
197
|
if (!parentUnion) return true
|
|
167
198
|
const childRef = `${SCHEMA_REF_PREFIX}${name}`
|
|
168
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
199
|
+
const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
169
200
|
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
170
201
|
if (inOneOf || inMapping) {
|
|
171
202
|
const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef)
|
|
@@ -179,7 +210,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
179
210
|
}
|
|
180
211
|
return true
|
|
181
212
|
})
|
|
182
|
-
.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
|
|
213
|
+
.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))
|
|
183
214
|
|
|
184
215
|
const syntheticStart = allOfMembers.length
|
|
185
216
|
|
|
@@ -189,9 +220,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
189
220
|
|
|
190
221
|
if (missingRequired.length) {
|
|
191
222
|
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] : []
|
|
223
|
+
if (!dialect.isReference(item)) return [item as SchemaObject]
|
|
224
|
+
const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
|
|
225
|
+
return deref && !dialect.isReference(deref) ? [deref] : []
|
|
195
226
|
})
|
|
196
227
|
|
|
197
228
|
for (const key of missingRequired) {
|
|
@@ -204,6 +235,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
204
235
|
properties: { [key]: resolved.properties[key] },
|
|
205
236
|
required: [key],
|
|
206
237
|
} as SchemaObject,
|
|
238
|
+
name,
|
|
207
239
|
},
|
|
208
240
|
rawOptions,
|
|
209
241
|
),
|
|
@@ -217,6 +249,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
217
249
|
|
|
218
250
|
if (schema.properties) {
|
|
219
251
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
252
|
+
// Don't pass `name` here, the result must stay anonymous so it can be merged with the
|
|
253
|
+
// adjacent synthetic object in `mergeAdjacentObjectsLazy`. Nested enum qualification
|
|
254
|
+
// happens upstream via `convertObject`'s `setEnumName` propagation.
|
|
220
255
|
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
|
|
221
256
|
}
|
|
222
257
|
|
|
@@ -226,7 +261,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
226
261
|
|
|
227
262
|
return ast.createSchema({
|
|
228
263
|
type: 'intersection',
|
|
229
|
-
members: [...ast.
|
|
264
|
+
members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
|
|
230
265
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
231
266
|
})
|
|
232
267
|
}
|
|
@@ -254,10 +289,10 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
254
289
|
const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
|
|
255
290
|
const unionBase = {
|
|
256
291
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
257
|
-
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
292
|
+
discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
258
293
|
strategy,
|
|
259
294
|
}
|
|
260
|
-
const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
|
|
295
|
+
const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
|
|
261
296
|
const sharedPropertiesNode = schema.properties
|
|
262
297
|
? (() => {
|
|
263
298
|
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
@@ -270,9 +305,9 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
270
305
|
|
|
271
306
|
if (sharedPropertiesNode || discriminator?.mapping) {
|
|
272
307
|
const members = unionMembers.map((s) => {
|
|
273
|
-
const ref = isReference(s) ? s.$ref : undefined
|
|
308
|
+
const ref = dialect.isReference(s) ? s.$ref : undefined
|
|
274
309
|
const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref)
|
|
275
|
-
const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
|
|
310
|
+
const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
|
|
276
311
|
|
|
277
312
|
if (!discriminatorValue || !discriminator) {
|
|
278
313
|
return memberNode
|
|
@@ -322,7 +357,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
322
357
|
return ast.createSchema({
|
|
323
358
|
type: 'union',
|
|
324
359
|
...unionBase,
|
|
325
|
-
members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
|
|
360
|
+
members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))),
|
|
326
361
|
})
|
|
327
362
|
}
|
|
328
363
|
|
|
@@ -340,6 +375,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
340
375
|
title: schema.title,
|
|
341
376
|
description: schema.description,
|
|
342
377
|
deprecated: schema.deprecated,
|
|
378
|
+
format: schema.format,
|
|
343
379
|
})
|
|
344
380
|
}
|
|
345
381
|
|
|
@@ -454,6 +490,22 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
454
490
|
|
|
455
491
|
const nullInEnum = schema.enum!.includes(null)
|
|
456
492
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
493
|
+
|
|
494
|
+
// drf-spectacular `NullEnum` ({ enum: [null] }) is just `null`. An empty enum node would
|
|
495
|
+
// render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
|
|
496
|
+
// branch so it renders as a clean `null` (not `z.null().nullable()`).
|
|
497
|
+
if (nullInEnum && filteredValues.length === 0) {
|
|
498
|
+
return ast.createSchema({
|
|
499
|
+
type: 'null',
|
|
500
|
+
primitive: 'null',
|
|
501
|
+
name,
|
|
502
|
+
title: schema.title,
|
|
503
|
+
description: schema.description,
|
|
504
|
+
deprecated: schema.deprecated,
|
|
505
|
+
format: schema.format,
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
457
509
|
const enumNullable = nullable || nullInEnum || undefined
|
|
458
510
|
const enumDefault = schema.default === null && enumNullable ? undefined : schema.default
|
|
459
511
|
const enumPrimitive = getPrimitiveType(type)
|
|
@@ -470,6 +522,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
470
522
|
writeOnly: schema.writeOnly,
|
|
471
523
|
default: enumDefault,
|
|
472
524
|
example: schema.example,
|
|
525
|
+
format: schema.format,
|
|
473
526
|
}
|
|
474
527
|
|
|
475
528
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
@@ -513,19 +566,21 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
513
566
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
514
567
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
515
568
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
516
|
-
const propNullable = isNullable(resolvedPropSchema)
|
|
569
|
+
const propNullable = dialect.isNullable(resolvedPropSchema)
|
|
517
570
|
|
|
518
571
|
const resolvedChildName = ast.childName(name, propName)
|
|
519
572
|
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
573
|
+
const schemaNode = (() => {
|
|
574
|
+
const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
|
|
575
|
+
const tupleNode = ast.narrowSchema(node, 'tuple')
|
|
576
|
+
if (tupleNode?.items) {
|
|
577
|
+
const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
|
|
578
|
+
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
579
|
+
return { ...tupleNode, items: namedItems }
|
|
580
|
+
}
|
|
527
581
|
}
|
|
528
|
-
|
|
582
|
+
return node
|
|
583
|
+
})()
|
|
529
584
|
|
|
530
585
|
return ast.createProperty({
|
|
531
586
|
name: propName,
|
|
@@ -539,18 +594,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
539
594
|
: []
|
|
540
595
|
|
|
541
596
|
const additionalProperties = schema.additionalProperties
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
type: typeOptionMap.get(options.unknownType)!,
|
|
552
|
-
})
|
|
553
|
-
}
|
|
597
|
+
const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
|
|
598
|
+
if (additionalProperties === true) return true
|
|
599
|
+
if (additionalProperties === false) return false
|
|
600
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
601
|
+
return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
602
|
+
}
|
|
603
|
+
if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
604
|
+
return undefined
|
|
605
|
+
})()
|
|
554
606
|
|
|
555
607
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
556
608
|
|
|
@@ -578,7 +630,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
578
630
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
579
631
|
})
|
|
580
632
|
|
|
581
|
-
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
633
|
+
if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
582
634
|
const discPropName = schema.discriminator.propertyName
|
|
583
635
|
const values = Object.keys(schema.discriminator.mapping)
|
|
584
636
|
const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
|
|
@@ -616,7 +668,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
616
668
|
*/
|
|
617
669
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
618
670
|
const rawItems = schema.items as SchemaObject | undefined
|
|
619
|
-
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(
|
|
671
|
+
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(null, name, options.enumSuffix) : name
|
|
620
672
|
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
621
673
|
|
|
622
674
|
return ast.createSchema({
|
|
@@ -683,15 +735,90 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
683
735
|
description: schema.description,
|
|
684
736
|
deprecated: schema.deprecated,
|
|
685
737
|
nullable,
|
|
738
|
+
format: schema.format,
|
|
686
739
|
})
|
|
687
740
|
}
|
|
688
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
744
|
+
* into a `blob` node.
|
|
745
|
+
*/
|
|
746
|
+
function convertBlob({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
747
|
+
return ast.createSchema({
|
|
748
|
+
type: 'blob',
|
|
749
|
+
primitive: 'string',
|
|
750
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
756
|
+
*
|
|
757
|
+
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
|
|
758
|
+
* falls through and handles it as that single type with nullability already folded in.
|
|
759
|
+
*/
|
|
760
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode | null {
|
|
761
|
+
const types = schema.type as Array<string>
|
|
762
|
+
const nonNullTypes = types.filter((t) => t !== 'null')
|
|
763
|
+
if (nonNullTypes.length <= 1) return null
|
|
764
|
+
|
|
765
|
+
const arrayNullable = types.includes('null') || nullable || undefined
|
|
766
|
+
return ast.createSchema({
|
|
767
|
+
type: 'union',
|
|
768
|
+
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
769
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
775
|
+
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
776
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
777
|
+
* match/convert/fall-through contract.
|
|
778
|
+
*/
|
|
779
|
+
const schemaRules: Array<SchemaRule> = [
|
|
780
|
+
{ name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
|
|
781
|
+
{ name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
|
|
782
|
+
{ name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
|
|
783
|
+
{ name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
|
|
784
|
+
{ name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
|
|
785
|
+
{
|
|
786
|
+
name: 'blob',
|
|
787
|
+
match: ({ schema }) => dialect.isBinary(schema),
|
|
788
|
+
convert: convertBlob,
|
|
789
|
+
},
|
|
790
|
+
{ name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
|
|
791
|
+
{
|
|
792
|
+
name: 'constrained-string',
|
|
793
|
+
match: ({ schema, type }) => !type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined),
|
|
794
|
+
convert: convertString,
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: 'constrained-number',
|
|
798
|
+
match: ({ schema, type }) => !type && (schema.minimum !== undefined || schema.maximum !== undefined),
|
|
799
|
+
convert: (ctx) => convertNumeric(ctx, 'number'),
|
|
800
|
+
},
|
|
801
|
+
{ name: 'enum', match: ({ schema }) => !!schema.enum?.length, convert: convertEnum },
|
|
802
|
+
{
|
|
803
|
+
name: 'object',
|
|
804
|
+
match: ({ schema, type }) => type === 'object' || !!schema.properties || !!schema.additionalProperties || 'patternProperties' in schema,
|
|
805
|
+
convert: convertObject,
|
|
806
|
+
},
|
|
807
|
+
{ name: 'tuple', match: ({ schema }) => 'prefixItems' in schema, convert: convertTuple },
|
|
808
|
+
{ name: 'array', match: ({ schema, type }) => type === 'array' || 'items' in schema, convert: convertArray },
|
|
809
|
+
{ name: 'string', match: ({ type }) => type === 'string', convert: convertString },
|
|
810
|
+
{ name: 'number', match: ({ type }) => type === 'number', convert: (ctx) => convertNumeric(ctx, 'number') },
|
|
811
|
+
{ name: 'integer', match: ({ type }) => type === 'integer', convert: (ctx) => convertNumeric(ctx, 'integer') },
|
|
812
|
+
{ name: 'boolean', match: ({ type }) => type === 'boolean', convert: convertBoolean },
|
|
813
|
+
{ name: 'null', match: ({ type }) => type === 'null', convert: convertNull },
|
|
814
|
+
]
|
|
815
|
+
|
|
689
816
|
/**
|
|
690
817
|
* Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
691
818
|
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
819
|
+
* Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
|
|
820
|
+
* via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
|
|
821
|
+
* `emptySchemaType`.
|
|
695
822
|
*/
|
|
696
823
|
function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
|
|
697
824
|
const options: ast.ParserOptions = {
|
|
@@ -703,11 +830,11 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
703
830
|
return parseSchema({ schema: flattenedSchema, name }, rawOptions)
|
|
704
831
|
}
|
|
705
832
|
|
|
706
|
-
const nullable = isNullable(schema) || undefined
|
|
833
|
+
const nullable = dialect.isNullable(schema) || undefined
|
|
707
834
|
const defaultValue = schema.default === null && nullable ? undefined : schema.default
|
|
708
835
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
709
836
|
|
|
710
|
-
const
|
|
837
|
+
const schemaCtx: SchemaContext = {
|
|
711
838
|
schema,
|
|
712
839
|
name,
|
|
713
840
|
nullable,
|
|
@@ -717,58 +844,8 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
717
844
|
options,
|
|
718
845
|
}
|
|
719
846
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if (schema.allOf?.length) return convertAllOf(ctx)
|
|
723
|
-
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
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
|
|
731
|
-
}
|
|
732
|
-
|
|
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)
|
|
847
|
+
const node = ast.dispatch(schemaRules, schemaCtx)
|
|
848
|
+
if (node) return node
|
|
772
849
|
|
|
773
850
|
const emptyType = typeOptionMap.get(options.emptySchemaType)!
|
|
774
851
|
return ast.createSchema({
|
|
@@ -776,21 +853,24 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
776
853
|
name,
|
|
777
854
|
title: schema.title,
|
|
778
855
|
description: schema.description,
|
|
856
|
+
format: schema.format,
|
|
779
857
|
})
|
|
780
858
|
}
|
|
781
859
|
|
|
782
860
|
/**
|
|
783
861
|
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
784
862
|
*/
|
|
785
|
-
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown
|
|
863
|
+
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>, parentName?: string): ast.ParameterNode {
|
|
786
864
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
865
|
+
const paramName = param['name'] as string
|
|
866
|
+
const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : undefined
|
|
787
867
|
|
|
788
868
|
const schema: ast.SchemaNode = param['schema']
|
|
789
|
-
? parseSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
869
|
+
? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
|
|
790
870
|
: ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
791
871
|
|
|
792
872
|
return ast.createParameter({
|
|
793
|
-
name:
|
|
873
|
+
name: paramName,
|
|
794
874
|
in: param['in'] as ast.ParameterLocation,
|
|
795
875
|
schema: {
|
|
796
876
|
...schema,
|
|
@@ -839,25 +919,27 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
839
919
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
840
920
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
841
921
|
*/
|
|
842
|
-
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string
|
|
843
|
-
if (!schema?.properties) return
|
|
922
|
+
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): Array<string> | null {
|
|
923
|
+
if (!schema?.properties) return null
|
|
844
924
|
|
|
845
|
-
const keys: string
|
|
925
|
+
const keys: Array<string> = []
|
|
846
926
|
for (const key in schema.properties) {
|
|
847
927
|
const prop = schema.properties[key]
|
|
848
|
-
if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
928
|
+
if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
849
929
|
keys.push(key)
|
|
850
930
|
}
|
|
851
931
|
}
|
|
852
|
-
return keys.length ? keys :
|
|
932
|
+
return keys.length ? keys : null
|
|
853
933
|
}
|
|
854
934
|
|
|
855
935
|
/**
|
|
856
936
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
857
937
|
*/
|
|
858
938
|
function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
|
|
939
|
+
const operationId = operation.getOperationId()
|
|
940
|
+
const operationName = operationId ? pascalCase(operationId) : undefined
|
|
859
941
|
const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
|
|
860
|
-
parseParameter(options, param as unknown as Record<string, unknown
|
|
942
|
+
parseParameter(options, param as unknown as Record<string, unknown>, operationName),
|
|
861
943
|
)
|
|
862
944
|
|
|
863
945
|
// Determine which content types to include in requestBody.content.
|
|
@@ -866,6 +948,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
866
948
|
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation)
|
|
867
949
|
|
|
868
950
|
const requestBodyMeta = getRequestBodyMeta(operation)
|
|
951
|
+
const requestBodyName = operationName ? `${operationName}Request` : undefined
|
|
869
952
|
|
|
870
953
|
const content = allContentTypes.flatMap((ct) => {
|
|
871
954
|
const schema = getRequestSchema(document, operation, { contentType: ct })
|
|
@@ -873,7 +956,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
873
956
|
return [
|
|
874
957
|
{
|
|
875
958
|
contentType: ct,
|
|
876
|
-
schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
|
|
959
|
+
schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
|
|
877
960
|
keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
|
|
878
961
|
},
|
|
879
962
|
]
|
|
@@ -890,31 +973,45 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
890
973
|
|
|
891
974
|
const responses: Array<ast.ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
|
|
892
975
|
const responseObj = operation.getResponseByStatusCode(statusCode)
|
|
893
|
-
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
|
|
894
976
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
977
|
+
// Use `Status<code>` (matching plugin-ts's resolveResponseStatusName convention) so the
|
|
978
|
+
// qualified names for nested enums don't collide with top-level component schemas that
|
|
979
|
+
// happen to be named `<operation><statusCode>` (e.g. `GetMaintenance200`).
|
|
980
|
+
const responseName = operationName ? `${operationName}Status${statusCode}` : undefined
|
|
981
|
+
const { description } = getResponseMeta(responseObj)
|
|
982
|
+
|
|
983
|
+
const parseEntrySchema = (contentType?: string) => {
|
|
984
|
+
const raw = getResponseSchema(document, operation, statusCode, { contentType })
|
|
985
|
+
const node =
|
|
986
|
+
raw && Object.keys(raw).length > 0
|
|
987
|
+
? parseSchema({ schema: raw, name: responseName }, options)
|
|
988
|
+
: ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
989
|
+
return { schema: node, keysToOmit: collectPropertyKeysByFlag(raw, 'writeOnly') }
|
|
990
|
+
}
|
|
901
991
|
|
|
902
|
-
|
|
903
|
-
|
|
992
|
+
// Build one entry per declared response content type so plugins can union the variants.
|
|
993
|
+
// When a global contentType is configured, restrict to that single type (mirrors requestBody).
|
|
994
|
+
const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
|
|
995
|
+
const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
|
|
996
|
+
|
|
997
|
+
// Body-less responses keep a single fallback entry so the response still resolves to a
|
|
998
|
+
// (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
|
|
999
|
+
if (content.length === 0) {
|
|
1000
|
+
content.push({ contentType: operation.contentType || 'application/json', ...parseEntrySchema(ctx.contentType) })
|
|
1001
|
+
}
|
|
904
1002
|
|
|
905
1003
|
return ast.createResponse({
|
|
906
1004
|
statusCode: statusCode as ast.StatusCode,
|
|
907
1005
|
description,
|
|
908
|
-
|
|
909
|
-
mediaType,
|
|
910
|
-
keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
|
|
1006
|
+
content,
|
|
911
1007
|
})
|
|
912
1008
|
})
|
|
913
1009
|
|
|
914
1010
|
const urlPath = new URLPath(operation.path)
|
|
915
1011
|
|
|
916
1012
|
return ast.createOperation({
|
|
917
|
-
operationId
|
|
1013
|
+
operationId,
|
|
1014
|
+
protocol: 'http',
|
|
918
1015
|
method: operation.method.toUpperCase() as ast.HttpMethod,
|
|
919
1016
|
path: urlPath.path,
|
|
920
1017
|
tags: operation.getTags().map((tag) => tag.name),
|
|
@@ -957,8 +1054,7 @@ export function parseSchema(
|
|
|
957
1054
|
* Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
|
|
958
1055
|
*
|
|
959
1056
|
* This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
|
|
960
|
-
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here
|
|
961
|
-
* the tree is a pure data structure representing all schemas and operations.
|
|
1057
|
+
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here, * the tree is a pure data structure representing all schemas and operations.
|
|
962
1058
|
*
|
|
963
1059
|
* Returns the AST root and a `nameMapping` for resolving schema references.
|
|
964
1060
|
*
|
|
@@ -990,7 +1086,7 @@ export function parseOas(
|
|
|
990
1086
|
const baseOas = new BaseOas(document)
|
|
991
1087
|
const paths = baseOas.getPaths()
|
|
992
1088
|
|
|
993
|
-
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([
|
|
1089
|
+
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
|
|
994
1090
|
Object.entries(methods)
|
|
995
1091
|
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
996
1092
|
.filter((op): op is ast.OperationNode => op !== null),
|