@kubb/adapter-oas 5.0.0-alpha.12 → 5.0.0-alpha.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-alpha.12",
3
+ "version": "5.0.0-alpha.13",
4
4
  "description": "OpenAPI / Swagger adapter for Kubb — converts OAS input into a @kubb/ast RootNode.",
5
5
  "keywords": [
6
6
  "openapi",
@@ -46,8 +46,8 @@
46
46
  "openapi-types": "^12.1.3",
47
47
  "remeda": "^2.33.6",
48
48
  "swagger2openapi": "^7.0.8",
49
- "@kubb/ast": "5.0.0-alpha.12",
50
- "@kubb/core": "5.0.0-alpha.12"
49
+ "@kubb/ast": "5.0.0-alpha.13",
50
+ "@kubb/core": "5.0.0-alpha.13"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
package/src/adapter.ts CHANGED
@@ -87,15 +87,14 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
87
87
  const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
88
88
 
89
89
  const parser = createOasParser(oas, { contentType, collisionDetection })
90
+ const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType, enumSuffix })
90
91
 
91
- // Sync the adapter's shared nameMapping with the one computed by the parser.
92
+ // This must happen after parse() because legacy enum remapping is finalized there.
92
93
  nameMapping.clear()
93
94
  for (const [key, value] of parser.nameMapping) {
94
95
  nameMapping.set(key, value)
95
96
  }
96
97
 
97
- const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType, enumSuffix })
98
-
99
98
  return createRoot({
100
99
  ...root,
101
100
  meta: {
package/src/parser.ts CHANGED
@@ -367,6 +367,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
367
367
 
368
368
  // When a child schema extends a discriminator parent via allOf and the parent's oneOf/anyOf
369
369
  // references that child back, skip that allOf item to prevent a circular type reference.
370
+ // When an item is skipped, collect its discriminant value so it can be injected below.
371
+ const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
370
372
  const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
371
373
  .filter((item) => {
372
374
  if (!isReference(item) || !name) return true
@@ -377,12 +379,18 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
377
379
  const childRef = `#/components/schemas/${name}`
378
380
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
379
381
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
380
- return !inOneOf && !inMapping
382
+ if (inOneOf || inMapping) {
383
+ const discriminatorValue = Object.entries(deref.discriminator.mapping ?? {}).find(([, v]) => v === childRef)?.[0]
384
+ if (discriminatorValue) {
385
+ filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
386
+ }
387
+ return false
388
+ }
389
+ return true
381
390
  })
382
391
  .map((s) => convertSchema({ schema: s as SchemaObject }, options))
383
392
 
384
- // Track where allOf-derived members end so only the synthetic members added below
385
- // (injected required-key objects + outer-properties object) are candidates for merging.
393
+ // Track where allOf-derived members end so each portion can be merged independently.
386
394
  const syntheticStart = allOfMembers.length
387
395
 
388
396
  // When `required` lists keys not present in the outer `properties`, resolve them from
@@ -414,10 +422,32 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
414
422
  allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options))
415
423
  }
416
424
 
425
+ // Inject a synthetic single-property object for each discriminant value collected from
426
+ // filtered discriminator parents so that child schemas carry the narrowed literal type.
427
+ for (const { propertyName, value } of filteredDiscriminantValues) {
428
+ allOfMembers.push(
429
+ createSchema({
430
+ type: 'object',
431
+ primitive: 'object',
432
+ properties: [
433
+ createProperty({
434
+ name: propertyName,
435
+ schema: createSchema({
436
+ type: 'enum',
437
+ primitive: 'string',
438
+ enumValues: [value],
439
+ }),
440
+ required: true,
441
+ }),
442
+ ],
443
+ }),
444
+ )
445
+ }
446
+
417
447
  // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
418
448
  return createSchema({
419
449
  type: 'intersection',
420
- members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
450
+ members: [...mergeAdjacentAnonymousObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
421
451
  ...renderSchemaBase(schema, name, nullable, defaultValue),
422
452
  })
423
453
  }
@@ -471,6 +501,45 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
471
501
  })
472
502
  }
473
503
 
504
+ // When a discriminator with mapping is present but there are no sibling properties,
505
+ // embed the narrowed discriminant value into each member to produce precise literal
506
+ // intersection types (e.g. `Cat & { type: 'cat' }`) instead of plain `Cat | Dog`.
507
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
508
+ if (discriminator?.mapping) {
509
+ return createSchema({
510
+ type: 'union',
511
+ ...unionBase,
512
+ members: unionMembers.map((s) => {
513
+ const ref = isReference(s) ? s.$ref : undefined
514
+ const discriminatorValue = ref ? Object.entries(discriminator.mapping!).find(([, v]) => v === ref)?.[0] : undefined
515
+ const memberNode = convertSchema({ schema: s as SchemaObject }, options)
516
+
517
+ if (!discriminatorValue) return memberNode
518
+
519
+ const discriminantNode = createSchema({
520
+ type: 'object',
521
+ primitive: 'object',
522
+ properties: [
523
+ createProperty({
524
+ name: discriminator.propertyName,
525
+ schema: createSchema({
526
+ type: 'enum',
527
+ primitive: 'string',
528
+ enumValues: [discriminatorValue],
529
+ }),
530
+ required: true,
531
+ }),
532
+ ],
533
+ })
534
+
535
+ return createSchema({
536
+ type: 'intersection',
537
+ members: [memberNode, discriminantNode],
538
+ })
539
+ }),
540
+ })
541
+ }
542
+
474
543
  return createSchema({
475
544
  type: 'union',
476
545
  ...unionBase,
@@ -487,6 +556,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
487
556
  const constValue = schema.const
488
557
 
489
558
  if (constValue === null) {
559
+ // Do not propagate `nullable` here: the type is already `null`, so marking it
560
+ // nullable too would cause the printer to emit `null | null`.
490
561
  return createSchema({
491
562
  type: 'null',
492
563
  primitive: 'null',
@@ -494,7 +565,6 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
494
565
  title: schema.title,
495
566
  description: schema.description,
496
567
  deprecated: schema.deprecated,
497
- nullable,
498
568
  })
499
569
  }
500
570
 
@@ -719,13 +789,21 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
719
789
 
720
790
  const childName = resolveChildName(name, propName)
721
791
  const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, options)
722
- const schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
792
+ let schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
793
+
794
+ const tupleNode = narrowSchema(schemaNode, 'tuple')
795
+ if (tupleNode?.items) {
796
+ const namedItems = tupleNode.items.map((item) => applyEnumName(item, name, propName, mergedOptions.enumSuffix))
797
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
798
+ schemaNode = { ...tupleNode, items: namedItems }
799
+ }
800
+ }
723
801
 
724
802
  return createProperty({
725
803
  name: propName,
726
804
  schema: {
727
805
  ...schemaNode,
728
- nullable: propNullable || undefined,
806
+ nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
729
807
  optional: !required && !propNullable ? true : undefined,
730
808
  nullish: !required && propNullable ? true : undefined,
731
809
  },
@@ -783,12 +861,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
783
861
  /**
784
862
  * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
785
863
  *
786
- * Each `prefixItems` element maps to a positional tuple slot. An optional `items` schema
787
- * after the prefix items is mapped to the rest parameter of the tuple.
864
+ * Each `prefixItems` element maps to a positional tuple slot. When an explicit `items` schema
865
+ * is present alongside `prefixItems`, it becomes the rest element. When `items` is absent,
866
+ * a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
867
+ * means additional items are allowed).
788
868
  */
789
869
  function convertTuple({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
790
870
  const tupleItems = (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item as SchemaObject }, options))
791
- const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, options) : undefined
871
+ const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, options) : createSchema({ type: 'any' })
792
872
 
793
873
  return createSchema({
794
874
  type: 'tuple',
@@ -983,15 +1063,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
983
1063
 
984
1064
  /**
985
1065
  * Converts a single dereferenced OAS parameter object into a `ParameterNode`.
986
- * When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
1066
+ * When the parameter has no `schema`, falls back to `unknownType`; `$ref` schemas are resolved through `convertSchema` to produce a proper named type reference.
987
1067
  */
988
1068
  function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
989
1069
  const required = (param['required'] as boolean | undefined) ?? false
990
1070
 
991
- const schema: SchemaNode =
992
- param['schema'] && !isReference(param['schema'])
993
- ? convertSchema({ schema: param['schema'] as SchemaObject }, options)
994
- : createSchema({ type: resolveTypeOption(options.unknownType) })
1071
+ const schema: SchemaNode = param['schema']
1072
+ ? convertSchema({ schema: param['schema'] as SchemaObject }, options)
1073
+ : createSchema({ type: resolveTypeOption(options.unknownType) })
995
1074
 
996
1075
  return createParameter({
997
1076
  name: param['name'] as string,
@@ -1015,6 +1094,11 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1015
1094
  const requestBodySchema = oas.getRequestSchema(operation)
1016
1095
  const requestBodySchemaNode = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
1017
1096
 
1097
+ const requestBodyDescription =
1098
+ operation.schema.requestBody && !isReference(operation.schema.requestBody)
1099
+ ? (operation.schema.requestBody as { description?: string }).description
1100
+ : undefined
1101
+
1018
1102
  const requestBodyKeysToOmit = requestBodySchema?.properties
1019
1103
  ? Object.entries(requestBodySchema.properties)
1020
1104
  .filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
@@ -1023,6 +1107,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1023
1107
 
1024
1108
  const requestBody = requestBodySchemaNode
1025
1109
  ? {
1110
+ description: requestBodyDescription,
1026
1111
  schema: requestBodySchemaNode,
1027
1112
  keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
1028
1113
  }