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