@kubb/adapter-oas 5.0.0-beta.3 → 5.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parser.ts CHANGED
@@ -1,18 +1,17 @@
1
- import { URLPath } from '@internals/utils'
1
+ import { pascalCase, URLPath } from '@internals/utils'
2
2
  import { ast } from '@kubb/core'
3
3
  import BaseOas from 'oas'
4
4
  import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, SCHEMA_REF_PREFIX, typeOptionMap } from './constants.ts'
5
- import { isDiscriminator, isNullable, isReference } from './guards.ts'
6
- import { resolveRef } from './refs.ts'
5
+ import { oasDialect, type OasDialect } from './dialect.ts'
7
6
  import {
8
7
  buildSchemaNode,
9
8
  flattenSchema,
10
9
  getDateType,
11
- getMediaType,
12
10
  getParameters,
13
11
  getPrimitiveType,
14
12
  getRequestBodyContentTypes,
15
13
  getRequestSchema,
14
+ getResponseBodyContentTypes,
16
15
  getResponseSchema,
17
16
  getSchemas,
18
17
  getSchemaType,
@@ -30,6 +29,16 @@ export type OasParserContext = {
30
29
  contentType?: ContentType
31
30
  }
32
31
 
32
+ /**
33
+ * The object returned by {@link createSchemaParser}.
34
+ * Contains parser functions bound to a specific document.
35
+ */
36
+ export type SchemaParser = {
37
+ parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
38
+ parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
39
+ parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
40
+ }
41
+
33
42
  /**
34
43
  * Pre-computed per-schema context passed to every schema converter.
35
44
  *
@@ -50,6 +59,13 @@ type SchemaContext = {
50
59
  options: ast.ParserOptions
51
60
  }
52
61
 
62
+ /**
63
+ * One entry in this adapter's schema-dispatch table — a {@link ast.DispatchRule} specialized
64
+ * to OAS schema context and Kubb `SchemaNode` output. See {@link ast.dispatch} for the contract
65
+ * a future adapter (e.g. AsyncAPI) follows: define a context type and an ordered rules table.
66
+ */
67
+ type SchemaRule = ast.DispatchRule<SchemaContext, ast.SchemaNode>
68
+
53
69
  /**
54
70
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
55
71
  *
@@ -76,9 +92,9 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
76
92
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
77
93
  * made possible by hoisting of function declarations.
78
94
  *
79
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
95
+ * @internal
80
96
  */
81
- function createSchemaParser(ctx: OasParserContext) {
97
+ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
82
98
  const document = ctx.document
83
99
 
84
100
  // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
@@ -89,6 +105,20 @@ function createSchemaParser(ctx: OasParserContext) {
89
105
  */
90
106
  const resolvingRefs = new Set<string>()
91
107
 
108
+ /**
109
+ * Cache of already-resolved `$ref` schemas within this parser instance.
110
+ *
111
+ * Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
112
+ * every time it appears as a `$ref` in a different parent schema. In heavily
113
+ * cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
114
+ * blowup — `customer` alone may be referenced from dozens of top-level schemas,
115
+ * each triggering a fresh recursive expansion of its entire sub-tree.
116
+ *
117
+ * Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
118
+ * where N is the number of unique schema names.
119
+ */
120
+ const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
121
+
92
122
  /**
93
123
  * Converts a `$ref` schema into a `RefSchemaNode`.
94
124
  *
@@ -98,19 +128,23 @@ function createSchemaParser(ctx: OasParserContext) {
98
128
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
99
129
  */
100
130
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
101
- let resolvedSchema: ast.SchemaNode | undefined
131
+ let resolvedSchema: ast.SchemaNode | null = null
102
132
  const refPath = schema.$ref
103
133
  if (refPath && !resolvingRefs.has(refPath)) {
104
- try {
105
- const referenced = resolveRef<SchemaObject>(document, refPath)
106
- if (referenced) {
107
- resolvingRefs.add(refPath)
108
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
109
- resolvingRefs.delete(refPath)
134
+ if (!resolvedRefCache.has(refPath)) {
135
+ try {
136
+ const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
137
+ if (referenced) {
138
+ resolvingRefs.add(refPath)
139
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
140
+ resolvingRefs.delete(refPath)
141
+ }
142
+ } catch {
143
+ // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
110
144
  }
111
- } catch {
112
- // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
145
+ resolvedRefCache.set(refPath, resolvedSchema)
113
146
  }
147
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null
114
148
  }
115
149
 
116
150
  return ast.createSchema({
@@ -133,7 +167,7 @@ function createSchemaParser(ctx: OasParserContext) {
133
167
  schema.additionalProperties === undefined
134
168
  ) {
135
169
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
136
- const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
170
+ const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name }, rawOptions)
137
171
  const { kind: _kind, ...memberNodeProps } = memberNode
138
172
  const mergedNullable = nullable || memberNode.nullable || undefined
139
173
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -150,6 +184,7 @@ function createSchemaParser(ctx: OasParserContext) {
150
184
  default: mergedDefault,
151
185
  example: schema.example ?? memberNode.example,
152
186
  pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
187
+ format: schema.format ?? memberNode.format,
153
188
  } as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
154
189
  }
155
190
 
@@ -159,13 +194,13 @@ function createSchemaParser(ctx: OasParserContext) {
159
194
  }> = []
160
195
  const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
161
196
  .filter((item) => {
162
- if (!isReference(item) || !name) return true
163
- const deref = resolveRef<SchemaObject>(document, item.$ref)
164
- if (!deref || !isDiscriminator(deref)) return true
197
+ if (!dialect.isReference(item) || !name) return true
198
+ const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
199
+ if (!deref || !dialect.isDiscriminator(deref)) return true
165
200
  const parentUnion = deref.oneOf ?? deref.anyOf
166
201
  if (!parentUnion) return true
167
202
  const childRef = `${SCHEMA_REF_PREFIX}${name}`
168
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
203
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
169
204
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
170
205
  if (inOneOf || inMapping) {
171
206
  const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef)
@@ -179,7 +214,7 @@ function createSchemaParser(ctx: OasParserContext) {
179
214
  }
180
215
  return true
181
216
  })
182
- .map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
217
+ .map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))
183
218
 
184
219
  const syntheticStart = allOfMembers.length
185
220
 
@@ -189,9 +224,9 @@ function createSchemaParser(ctx: OasParserContext) {
189
224
 
190
225
  if (missingRequired.length) {
191
226
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
192
- if (!isReference(item)) return [item as SchemaObject]
193
- const deref = resolveRef<SchemaObject>(document, item.$ref)
194
- return deref && !isReference(deref) ? [deref] : []
227
+ if (!dialect.isReference(item)) return [item as SchemaObject]
228
+ const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
229
+ return deref && !dialect.isReference(deref) ? [deref] : []
195
230
  })
196
231
 
197
232
  for (const key of missingRequired) {
@@ -204,6 +239,7 @@ function createSchemaParser(ctx: OasParserContext) {
204
239
  properties: { [key]: resolved.properties[key] },
205
240
  required: [key],
206
241
  } as SchemaObject,
242
+ name,
207
243
  },
208
244
  rawOptions,
209
245
  ),
@@ -217,6 +253,9 @@ function createSchemaParser(ctx: OasParserContext) {
217
253
 
218
254
  if (schema.properties) {
219
255
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
256
+ // Don't pass `name` here — the result must stay anonymous so it can be merged with the
257
+ // adjacent synthetic object in `mergeAdjacentObjectsLazy`. Nested enum qualification
258
+ // happens upstream via `convertObject`'s `setEnumName` propagation.
220
259
  allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
221
260
  }
222
261
 
@@ -226,7 +265,7 @@ function createSchemaParser(ctx: OasParserContext) {
226
265
 
227
266
  return ast.createSchema({
228
267
  type: 'intersection',
229
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
268
+ members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
230
269
  ...buildSchemaNode(schema, name, nullable, defaultValue),
231
270
  })
232
271
  }
@@ -254,10 +293,10 @@ function createSchemaParser(ctx: OasParserContext) {
254
293
  const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
255
294
  const unionBase = {
256
295
  ...buildSchemaNode(schema, name, nullable, defaultValue),
257
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
296
+ discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
258
297
  strategy,
259
298
  }
260
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
299
+ const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
261
300
  const sharedPropertiesNode = schema.properties
262
301
  ? (() => {
263
302
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
@@ -270,9 +309,9 @@ function createSchemaParser(ctx: OasParserContext) {
270
309
 
271
310
  if (sharedPropertiesNode || discriminator?.mapping) {
272
311
  const members = unionMembers.map((s) => {
273
- const ref = isReference(s) ? s.$ref : undefined
312
+ const ref = dialect.isReference(s) ? s.$ref : undefined
274
313
  const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref)
275
- const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
314
+ const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
276
315
 
277
316
  if (!discriminatorValue || !discriminator) {
278
317
  return memberNode
@@ -322,7 +361,7 @@ function createSchemaParser(ctx: OasParserContext) {
322
361
  return ast.createSchema({
323
362
  type: 'union',
324
363
  ...unionBase,
325
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
364
+ members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))),
326
365
  })
327
366
  }
328
367
 
@@ -340,6 +379,7 @@ function createSchemaParser(ctx: OasParserContext) {
340
379
  title: schema.title,
341
380
  description: schema.description,
342
381
  deprecated: schema.deprecated,
382
+ format: schema.format,
343
383
  })
344
384
  }
345
385
 
@@ -470,6 +510,7 @@ function createSchemaParser(ctx: OasParserContext) {
470
510
  writeOnly: schema.writeOnly,
471
511
  default: enumDefault,
472
512
  example: schema.example,
513
+ format: schema.format,
473
514
  }
474
515
 
475
516
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
@@ -513,19 +554,21 @@ function createSchemaParser(ctx: OasParserContext) {
513
554
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
514
555
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
515
556
  const resolvedPropSchema = propSchema as SchemaObject
516
- const propNullable = isNullable(resolvedPropSchema)
557
+ const propNullable = dialect.isNullable(resolvedPropSchema)
517
558
 
518
559
  const resolvedChildName = ast.childName(name, propName)
519
560
  const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
520
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix)
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 }
561
+ const schemaNode = (() => {
562
+ const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
563
+ const tupleNode = ast.narrowSchema(node, 'tuple')
564
+ if (tupleNode?.items) {
565
+ const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
566
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
567
+ return { ...tupleNode, items: namedItems }
568
+ }
527
569
  }
528
- }
570
+ return node
571
+ })()
529
572
 
530
573
  return ast.createProperty({
531
574
  name: propName,
@@ -539,18 +582,15 @@ function createSchemaParser(ctx: OasParserContext) {
539
582
  : []
540
583
 
541
584
  const additionalProperties = schema.additionalProperties
542
- let additionalPropertiesNode: ast.SchemaNode | boolean | undefined
543
- if (additionalProperties === true) {
544
- additionalPropertiesNode = true
545
- } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
546
- additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
547
- } else if (additionalProperties === false) {
548
- additionalPropertiesNode = false
549
- } else if (additionalProperties) {
550
- additionalPropertiesNode = ast.createSchema({
551
- type: typeOptionMap.get(options.unknownType)!,
552
- })
553
- }
585
+ const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
586
+ if (additionalProperties === true) return true
587
+ if (additionalProperties === false) return false
588
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) {
589
+ return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
590
+ }
591
+ if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
592
+ return undefined
593
+ })()
554
594
 
555
595
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
556
596
 
@@ -578,7 +618,7 @@ function createSchemaParser(ctx: OasParserContext) {
578
618
  ...buildSchemaNode(schema, name, nullable, defaultValue),
579
619
  })
580
620
 
581
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
621
+ if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
582
622
  const discPropName = schema.discriminator.propertyName
583
623
  const values = Object.keys(schema.discriminator.mapping)
584
624
  const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
@@ -616,7 +656,7 @@ function createSchemaParser(ctx: OasParserContext) {
616
656
  */
617
657
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
618
658
  const rawItems = schema.items as SchemaObject | undefined
619
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(undefined, name, options.enumSuffix) : undefined
659
+ const itemName = rawItems?.enum?.length && name ? ast.enumPropName(null, name, options.enumSuffix) : name
620
660
  const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
621
661
 
622
662
  return ast.createSchema({
@@ -683,15 +723,90 @@ function createSchemaParser(ctx: OasParserContext) {
683
723
  description: schema.description,
684
724
  deprecated: schema.deprecated,
685
725
  nullable,
726
+ format: schema.format,
727
+ })
728
+ }
729
+
730
+ /**
731
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
732
+ * into a `blob` node.
733
+ */
734
+ function convertBlob({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
735
+ return ast.createSchema({
736
+ type: 'blob',
737
+ primitive: 'string',
738
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
686
739
  })
687
740
  }
688
741
 
742
+ /**
743
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
744
+ *
745
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
746
+ * falls through and handles it as that single type with nullability already folded in.
747
+ */
748
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode | null {
749
+ const types = schema.type as Array<string>
750
+ const nonNullTypes = types.filter((t) => t !== 'null')
751
+ if (nonNullTypes.length <= 1) return null
752
+
753
+ const arrayNullable = types.includes('null') || nullable || undefined
754
+ return ast.createSchema({
755
+ type: 'union',
756
+ members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
757
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
758
+ })
759
+ }
760
+
761
+ /**
762
+ * Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
763
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
764
+ * `type`. The first matching rule that produces a node wins; see {@link SchemaRule} for the
765
+ * match/convert/fall-through contract.
766
+ */
767
+ const schemaRules: Array<SchemaRule> = [
768
+ { name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
769
+ { name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
770
+ { name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
771
+ { name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
772
+ { name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
773
+ {
774
+ name: 'blob',
775
+ match: ({ schema }) => dialect.isBinary(schema),
776
+ convert: convertBlob,
777
+ },
778
+ { name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
779
+ {
780
+ name: 'constrained-string',
781
+ match: ({ schema, type }) => !type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined),
782
+ convert: convertString,
783
+ },
784
+ {
785
+ name: 'constrained-number',
786
+ match: ({ schema, type }) => !type && (schema.minimum !== undefined || schema.maximum !== undefined),
787
+ convert: (ctx) => convertNumeric(ctx, 'number'),
788
+ },
789
+ { name: 'enum', match: ({ schema }) => !!schema.enum?.length, convert: convertEnum },
790
+ {
791
+ name: 'object',
792
+ match: ({ schema, type }) => type === 'object' || !!schema.properties || !!schema.additionalProperties || 'patternProperties' in schema,
793
+ convert: convertObject,
794
+ },
795
+ { name: 'tuple', match: ({ schema }) => 'prefixItems' in schema, convert: convertTuple },
796
+ { name: 'array', match: ({ schema, type }) => type === 'array' || 'items' in schema, convert: convertArray },
797
+ { name: 'string', match: ({ type }) => type === 'string', convert: convertString },
798
+ { name: 'number', match: ({ type }) => type === 'number', convert: (ctx) => convertNumeric(ctx, 'number') },
799
+ { name: 'integer', match: ({ type }) => type === 'integer', convert: (ctx) => convertNumeric(ctx, 'integer') },
800
+ { name: 'boolean', match: ({ type }) => type === 'boolean', convert: convertBoolean },
801
+ { name: 'null', match: ({ type }) => type === 'null', convert: convertNull },
802
+ ]
803
+
689
804
  /**
690
805
  * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
691
806
  *
692
- * Dispatch order (first match wins): `$ref` `allOf` `oneOf`/`anyOf` `const` → `format`
693
- * octet-stream blob multi-type array constraint-inferred type `enum` object/array/tuple/scalar
694
- * → empty-schema fallback (`emptySchemaType` option).
807
+ * Builds the per-schema context, then runs it through the ordered {@link schemaRules} table
808
+ * via {@link ast.dispatch}. When no rule produces a node, falls back to the configured
809
+ * `emptySchemaType`.
695
810
  */
696
811
  function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
697
812
  const options: ast.ParserOptions = {
@@ -703,11 +818,11 @@ function createSchemaParser(ctx: OasParserContext) {
703
818
  return parseSchema({ schema: flattenedSchema, name }, rawOptions)
704
819
  }
705
820
 
706
- const nullable = isNullable(schema) || undefined
821
+ const nullable = dialect.isNullable(schema) || undefined
707
822
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
708
823
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
709
824
 
710
- const ctx: SchemaContext = {
825
+ const schemaCtx: SchemaContext = {
711
826
  schema,
712
827
  name,
713
828
  nullable,
@@ -717,58 +832,8 @@ function createSchemaParser(ctx: OasParserContext) {
717
832
  options,
718
833
  }
719
834
 
720
- if (isReference(schema)) return convertRef(ctx)
721
-
722
- if (schema.allOf?.length) return convertAllOf(ctx)
723
- const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
724
- if (unionMembers.length) return convertUnion(ctx)
725
-
726
- if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
727
-
728
- if (schema.format) {
729
- const formatResult = convertFormat(ctx)
730
- if (formatResult) return formatResult
731
- }
732
-
733
- if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
734
- return ast.createSchema({
735
- type: 'blob',
736
- primitive: 'string',
737
- ...buildSchemaNode(schema, name, nullable, defaultValue),
738
- })
739
- }
740
-
741
- if (Array.isArray(schema.type) && schema.type.length > 1) {
742
- const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
743
- const arrayNullable = schema.type.includes('null') || nullable || undefined
744
-
745
- if (nonNullTypes.length > 1) {
746
- return ast.createSchema({
747
- type: 'union',
748
- members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
749
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
750
- })
751
- }
752
- }
753
-
754
- if (!type) {
755
- if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
756
- return convertString(ctx)
757
- }
758
- if (schema.minimum !== undefined || schema.maximum !== undefined) {
759
- return convertNumeric(ctx, 'number')
760
- }
761
- }
762
-
763
- if (schema.enum?.length) return convertEnum(ctx)
764
- if (type === 'object' || schema.properties || schema.additionalProperties || 'patternProperties' in schema) return convertObject(ctx)
765
- if ('prefixItems' in schema) return convertTuple(ctx)
766
- if (type === 'array' || 'items' in schema) return convertArray(ctx)
767
- if (type === 'string') return convertString(ctx)
768
- if (type === 'number') return convertNumeric(ctx, 'number')
769
- if (type === 'integer') return convertNumeric(ctx, 'integer')
770
- if (type === 'boolean') return convertBoolean(ctx)
771
- if (type === 'null') return convertNull(ctx)
835
+ const node = ast.dispatch(schemaRules, schemaCtx)
836
+ if (node) return node
772
837
 
773
838
  const emptyType = typeOptionMap.get(options.emptySchemaType)!
774
839
  return ast.createSchema({
@@ -776,21 +841,24 @@ function createSchemaParser(ctx: OasParserContext) {
776
841
  name,
777
842
  title: schema.title,
778
843
  description: schema.description,
844
+ format: schema.format,
779
845
  })
780
846
  }
781
847
 
782
848
  /**
783
849
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
784
850
  */
785
- function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>): ast.ParameterNode {
851
+ function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>, parentName?: string): ast.ParameterNode {
786
852
  const required = (param['required'] as boolean | undefined) ?? false
853
+ const paramName = param['name'] as string
854
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : undefined
787
855
 
788
856
  const schema: ast.SchemaNode = param['schema']
789
- ? parseSchema({ schema: param['schema'] as SchemaObject }, options)
857
+ ? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
790
858
  : ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
791
859
 
792
860
  return ast.createParameter({
793
- name: param['name'] as string,
861
+ name: paramName,
794
862
  in: param['in'] as ast.ParameterLocation,
795
863
  schema: {
796
864
  ...schema,
@@ -839,25 +907,27 @@ function createSchemaParser(ctx: OasParserContext) {
839
907
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
840
908
  * `$ref` entries are skipped since their flags live on the dereferenced target.
841
909
  */
842
- function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | undefined {
843
- if (!schema?.properties) return undefined
910
+ function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): Array<string> | null {
911
+ if (!schema?.properties) return null
844
912
 
845
- const keys: string[] = []
913
+ const keys: Array<string> = []
846
914
  for (const key in schema.properties) {
847
915
  const prop = schema.properties[key]
848
- if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
916
+ if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
849
917
  keys.push(key)
850
918
  }
851
919
  }
852
- return keys.length ? keys : undefined
920
+ return keys.length ? keys : null
853
921
  }
854
922
 
855
923
  /**
856
924
  * Converts an OAS `Operation` into an `OperationNode`.
857
925
  */
858
926
  function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
927
+ const operationId = operation.getOperationId()
928
+ const operationName = operationId ? pascalCase(operationId) : undefined
859
929
  const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
860
- parseParameter(options, param as unknown as Record<string, unknown>),
930
+ parseParameter(options, param as unknown as Record<string, unknown>, operationName),
861
931
  )
862
932
 
863
933
  // Determine which content types to include in requestBody.content.
@@ -866,6 +936,7 @@ function createSchemaParser(ctx: OasParserContext) {
866
936
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation)
867
937
 
868
938
  const requestBodyMeta = getRequestBodyMeta(operation)
939
+ const requestBodyName = operationName ? `${operationName}Request` : undefined
869
940
 
870
941
  const content = allContentTypes.flatMap((ct) => {
871
942
  const schema = getRequestSchema(document, operation, { contentType: ct })
@@ -873,7 +944,7 @@ function createSchemaParser(ctx: OasParserContext) {
873
944
  return [
874
945
  {
875
946
  contentType: ct,
876
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
947
+ schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
877
948
  keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
878
949
  },
879
950
  ]
@@ -890,31 +961,45 @@ function createSchemaParser(ctx: OasParserContext) {
890
961
 
891
962
  const responses: Array<ast.ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
892
963
  const responseObj = operation.getResponseByStatusCode(statusCode)
893
- const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
894
964
 
895
- const schema =
896
- responseSchema && Object.keys(responseSchema).length > 0
897
- ? parseSchema({ schema: responseSchema }, options)
898
- : ast.createSchema({
899
- type: typeOptionMap.get(options.emptySchemaType)!,
900
- })
965
+ // Use `Status<code>` (matching plugin-ts's resolveResponseStatusName convention) so the
966
+ // qualified names for nested enums don't collide with top-level component schemas that
967
+ // happen to be named `<operation><statusCode>` (e.g. `GetMaintenance200`).
968
+ const responseName = operationName ? `${operationName}Status${statusCode}` : undefined
969
+ const { description } = getResponseMeta(responseObj)
970
+
971
+ const parseEntrySchema = (contentType?: string) => {
972
+ const raw = getResponseSchema(document, operation, statusCode, { contentType })
973
+ const node =
974
+ raw && Object.keys(raw).length > 0
975
+ ? parseSchema({ schema: raw, name: responseName }, options)
976
+ : ast.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
977
+ return { schema: node, keysToOmit: collectPropertyKeysByFlag(raw, 'writeOnly') }
978
+ }
979
+
980
+ // Build one entry per declared response content type so plugins can union the variants.
981
+ // When a global contentType is configured, restrict to that single type (mirrors requestBody).
982
+ const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
983
+ const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
901
984
 
902
- const { description, content } = getResponseMeta(responseObj)
903
- const mediaType = content ? getMediaType(Object.keys(content)[0] ?? '') : getMediaType(operation.contentType ?? '')
985
+ // Body-less responses keep a single fallback entry so the response still resolves to a
986
+ // (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
987
+ if (content.length === 0) {
988
+ content.push({ contentType: operation.contentType || 'application/json', ...parseEntrySchema(ctx.contentType) })
989
+ }
904
990
 
905
991
  return ast.createResponse({
906
992
  statusCode: statusCode as ast.StatusCode,
907
993
  description,
908
- schema,
909
- mediaType,
910
- keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
994
+ content,
911
995
  })
912
996
  })
913
997
 
914
998
  const urlPath = new URLPath(operation.path)
915
999
 
916
1000
  return ast.createOperation({
917
- operationId: operation.getOperationId(),
1001
+ operationId,
1002
+ protocol: 'http',
918
1003
  method: operation.method.toUpperCase() as ast.HttpMethod,
919
1004
  path: urlPath.path,
920
1005
  tags: operation.getTags().map((tag) => tag.name),
@@ -990,7 +1075,7 @@ export function parseOas(
990
1075
  const baseOas = new BaseOas(document)
991
1076
  const paths = baseOas.getPaths()
992
1077
 
993
- const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
1078
+ const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
994
1079
  Object.entries(methods)
995
1080
  .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
996
1081
  .filter((op): op is ast.OperationNode => op !== null),