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