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