@kubb/adapter-oas 5.0.0-beta.3 → 5.0.0-beta.30
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 +778 -417
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +37 -66
- package/dist/index.js +778 -414
- 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 +90 -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 +219 -134
- package/src/refs.ts +16 -4
- package/src/resolvers.ts +50 -21
- package/src/stream.ts +175 -0
- package/src/types.ts +21 -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
|
|
|
@@ -470,6 +510,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
470
510
|
writeOnly: schema.writeOnly,
|
|
471
511
|
default: enumDefault,
|
|
472
512
|
example: schema.example,
|
|
513
|
+
format: schema.format,
|
|
473
514
|
}
|
|
474
515
|
|
|
475
516
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
@@ -513,19 +554,21 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
513
554
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
514
555
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
515
556
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
516
|
-
const propNullable = isNullable(resolvedPropSchema)
|
|
557
|
+
const propNullable = dialect.isNullable(resolvedPropSchema)
|
|
517
558
|
|
|
518
559
|
const resolvedChildName = ast.childName(name, propName)
|
|
519
560
|
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
561
|
+
const schemaNode = (() => {
|
|
562
|
+
const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
|
|
563
|
+
const tupleNode = ast.narrowSchema(node, 'tuple')
|
|
564
|
+
if (tupleNode?.items) {
|
|
565
|
+
const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
|
|
566
|
+
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
567
|
+
return { ...tupleNode, items: namedItems }
|
|
568
|
+
}
|
|
527
569
|
}
|
|
528
|
-
|
|
570
|
+
return node
|
|
571
|
+
})()
|
|
529
572
|
|
|
530
573
|
return ast.createProperty({
|
|
531
574
|
name: propName,
|
|
@@ -539,18 +582,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
539
582
|
: []
|
|
540
583
|
|
|
541
584
|
const additionalProperties = schema.additionalProperties
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
type: typeOptionMap.get(options.unknownType)!,
|
|
552
|
-
})
|
|
553
|
-
}
|
|
585
|
+
const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
|
|
586
|
+
if (additionalProperties === true) return true
|
|
587
|
+
if (additionalProperties === false) return false
|
|
588
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
589
|
+
return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
590
|
+
}
|
|
591
|
+
if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
592
|
+
return undefined
|
|
593
|
+
})()
|
|
554
594
|
|
|
555
595
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
556
596
|
|
|
@@ -578,7 +618,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
578
618
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
579
619
|
})
|
|
580
620
|
|
|
581
|
-
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
621
|
+
if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
582
622
|
const discPropName = schema.discriminator.propertyName
|
|
583
623
|
const values = Object.keys(schema.discriminator.mapping)
|
|
584
624
|
const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
|
|
@@ -616,7 +656,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
616
656
|
*/
|
|
617
657
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
618
658
|
const rawItems = schema.items as SchemaObject | undefined
|
|
619
|
-
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(
|
|
659
|
+
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(null, name, options.enumSuffix) : name
|
|
620
660
|
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
621
661
|
|
|
622
662
|
return ast.createSchema({
|
|
@@ -683,15 +723,90 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
683
723
|
description: schema.description,
|
|
684
724
|
deprecated: schema.deprecated,
|
|
685
725
|
nullable,
|
|
726
|
+
format: schema.format,
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
732
|
+
* into a `blob` node.
|
|
733
|
+
*/
|
|
734
|
+
function convertBlob({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
|
|
735
|
+
return ast.createSchema({
|
|
736
|
+
type: 'blob',
|
|
737
|
+
primitive: 'string',
|
|
738
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
686
739
|
})
|
|
687
740
|
}
|
|
688
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
744
|
+
*
|
|
745
|
+
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
|
|
746
|
+
* falls through and handles it as that single type with nullability already folded in.
|
|
747
|
+
*/
|
|
748
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode | null {
|
|
749
|
+
const types = schema.type as Array<string>
|
|
750
|
+
const nonNullTypes = types.filter((t) => t !== 'null')
|
|
751
|
+
if (nonNullTypes.length <= 1) return null
|
|
752
|
+
|
|
753
|
+
const arrayNullable = types.includes('null') || nullable || undefined
|
|
754
|
+
return ast.createSchema({
|
|
755
|
+
type: 'union',
|
|
756
|
+
members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
|
|
757
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue),
|
|
758
|
+
})
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
763
|
+
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
764
|
+
* `type`. The first matching rule that produces a node wins; see {@link SchemaRule} for the
|
|
765
|
+
* match/convert/fall-through contract.
|
|
766
|
+
*/
|
|
767
|
+
const schemaRules: Array<SchemaRule> = [
|
|
768
|
+
{ name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
|
|
769
|
+
{ name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
|
|
770
|
+
{ name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
|
|
771
|
+
{ name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
|
|
772
|
+
{ name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
|
|
773
|
+
{
|
|
774
|
+
name: 'blob',
|
|
775
|
+
match: ({ schema }) => dialect.isBinary(schema),
|
|
776
|
+
convert: convertBlob,
|
|
777
|
+
},
|
|
778
|
+
{ name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
|
|
779
|
+
{
|
|
780
|
+
name: 'constrained-string',
|
|
781
|
+
match: ({ schema, type }) => !type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined),
|
|
782
|
+
convert: convertString,
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'constrained-number',
|
|
786
|
+
match: ({ schema, type }) => !type && (schema.minimum !== undefined || schema.maximum !== undefined),
|
|
787
|
+
convert: (ctx) => convertNumeric(ctx, 'number'),
|
|
788
|
+
},
|
|
789
|
+
{ name: 'enum', match: ({ schema }) => !!schema.enum?.length, convert: convertEnum },
|
|
790
|
+
{
|
|
791
|
+
name: 'object',
|
|
792
|
+
match: ({ schema, type }) => type === 'object' || !!schema.properties || !!schema.additionalProperties || 'patternProperties' in schema,
|
|
793
|
+
convert: convertObject,
|
|
794
|
+
},
|
|
795
|
+
{ name: 'tuple', match: ({ schema }) => 'prefixItems' in schema, convert: convertTuple },
|
|
796
|
+
{ name: 'array', match: ({ schema, type }) => type === 'array' || 'items' in schema, convert: convertArray },
|
|
797
|
+
{ name: 'string', match: ({ type }) => type === 'string', convert: convertString },
|
|
798
|
+
{ name: 'number', match: ({ type }) => type === 'number', convert: (ctx) => convertNumeric(ctx, 'number') },
|
|
799
|
+
{ name: 'integer', match: ({ type }) => type === 'integer', convert: (ctx) => convertNumeric(ctx, 'integer') },
|
|
800
|
+
{ name: 'boolean', match: ({ type }) => type === 'boolean', convert: convertBoolean },
|
|
801
|
+
{ name: 'null', match: ({ type }) => type === 'null', convert: convertNull },
|
|
802
|
+
]
|
|
803
|
+
|
|
689
804
|
/**
|
|
690
805
|
* Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
691
806
|
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
807
|
+
* Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
|
|
808
|
+
* via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
|
|
809
|
+
* `emptySchemaType`.
|
|
695
810
|
*/
|
|
696
811
|
function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
|
|
697
812
|
const options: ast.ParserOptions = {
|
|
@@ -703,11 +818,11 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
703
818
|
return parseSchema({ schema: flattenedSchema, name }, rawOptions)
|
|
704
819
|
}
|
|
705
820
|
|
|
706
|
-
const nullable = isNullable(schema) || undefined
|
|
821
|
+
const nullable = dialect.isNullable(schema) || undefined
|
|
707
822
|
const defaultValue = schema.default === null && nullable ? undefined : schema.default
|
|
708
823
|
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
709
824
|
|
|
710
|
-
const
|
|
825
|
+
const schemaCtx: SchemaContext = {
|
|
711
826
|
schema,
|
|
712
827
|
name,
|
|
713
828
|
nullable,
|
|
@@ -717,58 +832,8 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
717
832
|
options,
|
|
718
833
|
}
|
|
719
834
|
|
|
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)
|
|
835
|
+
const node = ast.dispatch(schemaRules, schemaCtx)
|
|
836
|
+
if (node) return node
|
|
772
837
|
|
|
773
838
|
const emptyType = typeOptionMap.get(options.emptySchemaType)!
|
|
774
839
|
return ast.createSchema({
|
|
@@ -776,21 +841,24 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
776
841
|
name,
|
|
777
842
|
title: schema.title,
|
|
778
843
|
description: schema.description,
|
|
844
|
+
format: schema.format,
|
|
779
845
|
})
|
|
780
846
|
}
|
|
781
847
|
|
|
782
848
|
/**
|
|
783
849
|
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
784
850
|
*/
|
|
785
|
-
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown
|
|
851
|
+
function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>, parentName?: string): ast.ParameterNode {
|
|
786
852
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
853
|
+
const paramName = param['name'] as string
|
|
854
|
+
const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : undefined
|
|
787
855
|
|
|
788
856
|
const schema: ast.SchemaNode = param['schema']
|
|
789
|
-
? parseSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
857
|
+
? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
|
|
790
858
|
: ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
791
859
|
|
|
792
860
|
return ast.createParameter({
|
|
793
|
-
name:
|
|
861
|
+
name: paramName,
|
|
794
862
|
in: param['in'] as ast.ParameterLocation,
|
|
795
863
|
schema: {
|
|
796
864
|
...schema,
|
|
@@ -839,25 +907,27 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
839
907
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
840
908
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
841
909
|
*/
|
|
842
|
-
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string
|
|
843
|
-
if (!schema?.properties) return
|
|
910
|
+
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): Array<string> | null {
|
|
911
|
+
if (!schema?.properties) return null
|
|
844
912
|
|
|
845
|
-
const keys: string
|
|
913
|
+
const keys: Array<string> = []
|
|
846
914
|
for (const key in schema.properties) {
|
|
847
915
|
const prop = schema.properties[key]
|
|
848
|
-
if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
916
|
+
if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
|
|
849
917
|
keys.push(key)
|
|
850
918
|
}
|
|
851
919
|
}
|
|
852
|
-
return keys.length ? keys :
|
|
920
|
+
return keys.length ? keys : null
|
|
853
921
|
}
|
|
854
922
|
|
|
855
923
|
/**
|
|
856
924
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
857
925
|
*/
|
|
858
926
|
function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
|
|
927
|
+
const operationId = operation.getOperationId()
|
|
928
|
+
const operationName = operationId ? pascalCase(operationId) : undefined
|
|
859
929
|
const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
|
|
860
|
-
parseParameter(options, param as unknown as Record<string, unknown
|
|
930
|
+
parseParameter(options, param as unknown as Record<string, unknown>, operationName),
|
|
861
931
|
)
|
|
862
932
|
|
|
863
933
|
// Determine which content types to include in requestBody.content.
|
|
@@ -866,6 +936,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
866
936
|
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation)
|
|
867
937
|
|
|
868
938
|
const requestBodyMeta = getRequestBodyMeta(operation)
|
|
939
|
+
const requestBodyName = operationName ? `${operationName}Request` : undefined
|
|
869
940
|
|
|
870
941
|
const content = allContentTypes.flatMap((ct) => {
|
|
871
942
|
const schema = getRequestSchema(document, operation, { contentType: ct })
|
|
@@ -873,7 +944,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
873
944
|
return [
|
|
874
945
|
{
|
|
875
946
|
contentType: ct,
|
|
876
|
-
schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
|
|
947
|
+
schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
|
|
877
948
|
keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
|
|
878
949
|
},
|
|
879
950
|
]
|
|
@@ -890,31 +961,45 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
890
961
|
|
|
891
962
|
const responses: Array<ast.ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
|
|
892
963
|
const responseObj = operation.getResponseByStatusCode(statusCode)
|
|
893
|
-
const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
|
|
894
964
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
965
|
+
// Use `Status<code>` (matching plugin-ts's resolveResponseStatusName convention) so the
|
|
966
|
+
// qualified names for nested enums don't collide with top-level component schemas that
|
|
967
|
+
// happen to be named `<operation><statusCode>` (e.g. `GetMaintenance200`).
|
|
968
|
+
const responseName = operationName ? `${operationName}Status${statusCode}` : undefined
|
|
969
|
+
const { description } = getResponseMeta(responseObj)
|
|
970
|
+
|
|
971
|
+
const parseEntrySchema = (contentType?: string) => {
|
|
972
|
+
const raw = getResponseSchema(document, operation, statusCode, { contentType })
|
|
973
|
+
const node =
|
|
974
|
+
raw && Object.keys(raw).length > 0
|
|
975
|
+
? parseSchema({ schema: raw, name: responseName }, options)
|
|
976
|
+
: ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
|
|
977
|
+
return { schema: node, keysToOmit: collectPropertyKeysByFlag(raw, 'writeOnly') }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Build one entry per declared response content type so plugins can union the variants.
|
|
981
|
+
// When a global contentType is configured, restrict to that single type (mirrors requestBody).
|
|
982
|
+
const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
|
|
983
|
+
const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
|
|
901
984
|
|
|
902
|
-
|
|
903
|
-
|
|
985
|
+
// Body-less responses keep a single fallback entry so the response still resolves to a
|
|
986
|
+
// (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
|
|
987
|
+
if (content.length === 0) {
|
|
988
|
+
content.push({ contentType: operation.contentType || 'application/json', ...parseEntrySchema(ctx.contentType) })
|
|
989
|
+
}
|
|
904
990
|
|
|
905
991
|
return ast.createResponse({
|
|
906
992
|
statusCode: statusCode as ast.StatusCode,
|
|
907
993
|
description,
|
|
908
|
-
|
|
909
|
-
mediaType,
|
|
910
|
-
keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
|
|
994
|
+
content,
|
|
911
995
|
})
|
|
912
996
|
})
|
|
913
997
|
|
|
914
998
|
const urlPath = new URLPath(operation.path)
|
|
915
999
|
|
|
916
1000
|
return ast.createOperation({
|
|
917
|
-
operationId
|
|
1001
|
+
operationId,
|
|
1002
|
+
protocol: 'http',
|
|
918
1003
|
method: operation.method.toUpperCase() as ast.HttpMethod,
|
|
919
1004
|
path: urlPath.path,
|
|
920
1005
|
tags: operation.getTags().map((tag) => tag.name),
|
|
@@ -990,7 +1075,7 @@ export function parseOas(
|
|
|
990
1075
|
const baseOas = new BaseOas(document)
|
|
991
1076
|
const paths = baseOas.getPaths()
|
|
992
1077
|
|
|
993
|
-
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([
|
|
1078
|
+
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
|
|
994
1079
|
Object.entries(methods)
|
|
995
1080
|
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
996
1081
|
.filter((op): op is ast.OperationNode => op !== null),
|