@kubb/adapter-oas 5.0.0-beta.62 → 5.0.0-beta.64

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