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

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-beta.61",
3
+ "version": "5.0.0-beta.62",
4
4
  "description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
5
5
  "keywords": [
6
6
  "adapter",
@@ -46,8 +46,8 @@
46
46
  "api-ref-bundler": "0.5.1",
47
47
  "swagger2openapi": "^7.0.8",
48
48
  "yaml": "^2.9.0",
49
- "@kubb/ast": "5.0.0-beta.61",
50
- "@kubb/core": "5.0.0-beta.61"
49
+ "@kubb/ast": "5.0.0-beta.62",
50
+ "@kubb/core": "5.0.0-beta.62"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/json-schema": "^7.0.15",
package/src/dialect.ts CHANGED
@@ -3,6 +3,25 @@ import { isDiscriminator, isNullable, isReference } from './guards.ts'
3
3
  import { resolveRef } from './refs.ts'
4
4
  import type { SchemaObject } from './types.ts'
5
5
 
6
+ /**
7
+ * Derives a schema's `optional`/`nullish` flags from a parent's `required` value and the
8
+ * schema's own `nullable`. How "required" and "nullable" combine is OpenAPI-specific, so it
9
+ * lives in the dialect.
10
+ *
11
+ * - Non-required + non-nullable → `optional: true`.
12
+ * - Non-required + nullable → `nullish: true`.
13
+ * - Required → both flags cleared.
14
+ */
15
+ function optionality(schema: ast.SchemaNode, required: boolean): ast.SchemaNode {
16
+ const nullable = schema.nullable ?? false
17
+
18
+ return {
19
+ ...schema,
20
+ optional: !required && !nullable ? true : undefined,
21
+ nullish: !required && nullable ? true : undefined,
22
+ }
23
+ }
24
+
6
25
  /**
7
26
  * The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
8
27
  *
@@ -22,13 +41,16 @@ import type { SchemaObject } from './types.ts'
22
41
  * const parser = createSchemaParser(context, oasDialect) // explicit
23
42
  * ```
24
43
  */
25
- export const oasDialect = ast.defineSchemaDialect({
44
+ export const oasDialect = ast.defineDialect({
26
45
  name: 'oas',
27
- isNullable,
28
- isReference,
29
- isDiscriminator,
30
- isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
31
- resolveRef,
46
+ schema: {
47
+ isNullable,
48
+ isReference,
49
+ isDiscriminator,
50
+ isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
51
+ resolveRef,
52
+ optionality,
53
+ },
32
54
  })
33
55
 
34
56
  /**
package/src/parser.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getSchemaType,
21
21
  } from './resolvers.ts'
22
22
  import type { ContentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
23
+ import type { StatusCode } from '@kubb/ast'
23
24
 
24
25
  /**
25
26
  * Parser context holding the raw OpenAPI document and optional content-type override.
@@ -101,6 +102,22 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
101
102
  return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
102
103
  }
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
+
104
121
  /**
105
122
  * Names the inline enums on a property's schema, and on each item when the property is a tuple, from
106
123
  * the parent and property name. Wraps `macroEnumName` at the property construction site.
@@ -162,7 +179,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
162
179
  if (refPath && !resolvingRefs.has(refPath)) {
163
180
  if (!resolvedRefCache.has(refPath)) {
164
181
  try {
165
- const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
182
+ const referenced = dialect.schema.resolveRef<SchemaObject>(document, refPath)
166
183
  if (referenced) {
167
184
  resolvingRefs.add(refPath)
168
185
  resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
@@ -223,13 +240,13 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
223
240
  }> = []
224
241
  const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
225
242
  .filter((item) => {
226
- if (!dialect.isReference(item) || !name) return true
227
- const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
228
- if (!deref || !dialect.isDiscriminator(deref)) return true
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
229
246
  const parentUnion = deref.oneOf ?? deref.anyOf
230
247
  if (!parentUnion) return true
231
248
  const childRef = `${SCHEMA_REF_PREFIX}${name}`
232
- const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
249
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.schema.isReference(oneOfItem) && oneOfItem.$ref === childRef)
233
250
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
234
251
  if (inOneOf || inMapping) {
235
252
  const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
@@ -253,9 +270,9 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
253
270
 
254
271
  if (missingRequired.length) {
255
272
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
256
- if (!dialect.isReference(item)) return [item as SchemaObject]
257
- const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
258
- return deref && !dialect.isReference(deref) ? [deref] : []
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] : []
259
276
  })
260
277
 
261
278
  for (const key of missingRequired) {
@@ -322,10 +339,10 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
322
339
  const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
323
340
  const unionBase = {
324
341
  ...buildSchemaNode(schema, name, nullable, defaultValue),
325
- discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
342
+ discriminatorPropertyName: dialect.schema.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
326
343
  strategy,
327
344
  }
328
- const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
345
+ const discriminator = dialect.schema.isDiscriminator(schema) ? schema.discriminator : undefined
329
346
  const sharedPropertiesNode = schema.properties
330
347
  ? (() => {
331
348
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
@@ -338,7 +355,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
338
355
 
339
356
  if (sharedPropertiesNode || discriminator?.mapping) {
340
357
  const members = unionMembers.map((s) => {
341
- const ref = dialect.isReference(s) ? s.$ref : undefined
358
+ const ref = dialect.schema.isReference(s) ? s.$ref : undefined
342
359
  const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
343
360
  const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
344
361
 
@@ -401,15 +418,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
401
418
  const constValue = schema.const
402
419
 
403
420
  if (constValue === null) {
404
- return ast.factory.createSchema({
405
- type: 'null',
406
- primitive: 'null',
407
- name,
408
- title: schema.title,
409
- description: schema.description,
410
- deprecated: schema.deprecated,
411
- format: schema.format,
412
- })
421
+ return createNullSchema(schema, name)
413
422
  }
414
423
 
415
424
  const constPrimitive = getPrimitiveType(typeof constValue === 'number' ? 'number' : typeof constValue === 'boolean' ? 'boolean' : 'string')
@@ -528,15 +537,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
528
537
  // render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
529
538
  // branch so it renders as a clean `null` (not `z.null().nullable()`).
530
539
  if (nullInEnum && filteredValues.length === 0) {
531
- return ast.factory.createSchema({
532
- type: 'null',
533
- primitive: 'null',
534
- name,
535
- title: schema.title,
536
- description: schema.description,
537
- deprecated: schema.deprecated,
538
- format: schema.format,
539
- })
540
+ return createNullSchema(schema, name)
540
541
  }
541
542
 
542
543
  const enumNullable = nullable || nullInEnum || undefined
@@ -599,20 +600,23 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
599
600
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
600
601
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
601
602
  const resolvedPropSchema = propSchema as SchemaObject
602
- const propNullable = dialect.isNullable(resolvedPropSchema)
603
+ const propNullable = dialect.schema.isNullable(resolvedPropSchema)
603
604
 
604
605
  const resolvedChildName = childName(name, propName)
605
606
  const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
606
607
  const schemaNode = nameEnums(propNode, { parentName: name, propName, enumSuffix: options.enumSuffix })
607
608
 
608
- return ast.factory.createProperty({
609
- name: propName,
610
- schema: {
611
- ...schemaNode,
612
- nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
609
+ return ast.factory.createProperty(
610
+ {
611
+ name: propName,
612
+ schema: {
613
+ ...schemaNode,
614
+ nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
615
+ },
616
+ required,
613
617
  },
614
- required,
615
- })
618
+ dialect,
619
+ )
616
620
  })
617
621
  : []
618
622
 
@@ -653,7 +657,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
653
657
  ...buildSchemaNode(schema, name, nullable, defaultValue),
654
658
  })
655
659
 
656
- if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
660
+ if (dialect.schema.isDiscriminator(schema) && schema.discriminator.mapping) {
657
661
  const discPropName = schema.discriminator.propertyName
658
662
  const values = Object.keys(schema.discriminator.mapping)
659
663
  const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
@@ -795,14 +799,14 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
795
799
  * match/convert/fall-through contract.
796
800
  */
797
801
  const schemaRules: Array<SchemaRule> = [
798
- { name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
802
+ { name: 'ref', match: ({ schema }) => dialect.schema.isReference(schema), convert: convertRef },
799
803
  { name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
800
804
  { name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
801
805
  { name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
802
806
  { name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
803
807
  {
804
808
  name: 'blob',
805
- match: ({ schema }) => dialect.isBinary(schema),
809
+ match: ({ schema }) => dialect.schema.isBinary(schema),
806
810
  convert: convertBlob,
807
811
  },
808
812
  { name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
@@ -848,7 +852,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
848
852
  return parseSchema({ schema: flattenedSchema, name }, rawOptions)
849
853
  }
850
854
 
851
- const nullable = dialect.isNullable(schema) || undefined
855
+ const nullable = dialect.schema.isNullable(schema) || undefined
852
856
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
853
857
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
854
858
 
@@ -890,15 +894,18 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
890
894
  ? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
891
895
  : ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType)! })
892
896
 
893
- return ast.factory.createParameter({
894
- name: paramName,
895
- in: param['in'] as ast.ParameterLocation,
896
- schema: {
897
- ...schema,
898
- description: (param['description'] as string | undefined) ?? schema.description,
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,
899
906
  },
900
- required,
901
- })
907
+ dialect,
908
+ )
902
909
  }
903
910
 
904
911
  /**
@@ -946,7 +953,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
946
953
  const keys: Array<string> = []
947
954
  for (const key in schema.properties) {
948
955
  const prop = schema.properties[key]
949
- if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
956
+ if (prop && !dialect.schema.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
950
957
  keys.push(key)
951
958
  }
952
959
  }
@@ -975,11 +982,11 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
975
982
  const schema = getRequestSchema(document, operation, { contentType: ct })
976
983
  if (!schema) return []
977
984
  return [
978
- {
985
+ ast.factory.createContent({
979
986
  contentType: ct,
980
- schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
987
+ schema: dialect.schema.optionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
981
988
  keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
982
- },
989
+ }),
983
990
  ]
984
991
  })
985
992
 
@@ -1013,23 +1020,28 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
1013
1020
  // Build one entry per declared response content type so plugins can union the variants.
1014
1021
  // When a global contentType is configured, restrict to that single type (mirrors requestBody).
1015
1022
  const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
1016
- const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
1023
+ const content = responseContentTypes.map((contentType) => ast.factory.createContent({ contentType, ...parseEntrySchema(contentType) }))
1017
1024
 
1018
1025
  // Body-less responses keep a single fallback entry so the response still resolves to a
1019
1026
  // (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
1020
1027
  if (content.length === 0) {
1021
- content.push({ contentType: getRequestContentType({ document, operation }) || 'application/json', ...parseEntrySchema(ctx.contentType) })
1028
+ content.push(
1029
+ ast.factory.createContent({
1030
+ contentType: getRequestContentType({ document, operation }) || 'application/json',
1031
+ ...parseEntrySchema(ctx.contentType),
1032
+ }),
1033
+ )
1022
1034
  }
1023
1035
 
1024
1036
  return ast.factory.createResponse({
1025
- statusCode: statusCode as ast.StatusCode,
1037
+ statusCode: statusCode as StatusCode,
1026
1038
  description,
1027
1039
  content,
1028
1040
  })
1029
1041
  })
1030
1042
 
1031
1043
  const pathItem = document.paths?.[operation.path]
1032
- const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? (pathItem as { summary?: unknown; description?: unknown }) : undefined
1044
+ const pathItemDoc = pathItem && !dialect.schema.isReference(pathItem) ? (pathItem as { summary?: unknown; description?: unknown }) : undefined
1033
1045
  const pickDoc = (key: 'summary' | 'description'): string | undefined => {
1034
1046
  const own = operation.schema[key]
1035
1047
  if (typeof own === 'string') return own
package/src/stream.ts CHANGED
@@ -17,52 +17,6 @@ export type PreScanResult = {
17
17
  dedupePlan: ast.DedupePlan | null
18
18
  }
19
19
 
20
- /**
21
- * Builds the deduplication plan from the already-parsed top-level schema nodes plus a
22
- * single extra parse pass over operations (so duplicates in request/response bodies are seen).
23
- *
24
- * Only enums and objects are candidates, and object shapes that reference a circular schema are
25
- * rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
26
- * name (collision-resolved against existing component names). Shapes without a name stay inline.
27
- */
28
- function createDedupePlan({
29
- schemaNodes,
30
- operationNodes,
31
- schemaNames,
32
- circularNames,
33
- }: {
34
- schemaNodes: Array<ast.SchemaNode>
35
- operationNodes: Array<ast.OperationNode>
36
- schemaNames: Array<string>
37
- circularNames: Array<string>
38
- }): ast.DedupePlan {
39
- const circularSchemas = new Set(circularNames)
40
- const usedNames = new Set(schemaNames)
41
-
42
- return ast.buildDedupePlan([...schemaNodes, ...operationNodes], {
43
- isCandidate: (node) => {
44
- if (node.type === 'enum') return true
45
- if (node.type !== 'object') return false
46
- // Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
47
- if (node.name && circularSchemas.has(node.name)) return false
48
- return !containsCircularRef(node, { circularSchemas })
49
- },
50
- nameFor: (node) => {
51
- const base = node.name
52
- if (!base) return null
53
-
54
- let name = base
55
- let counter = 2
56
- while (usedNames.has(name)) {
57
- name = `${base}${counter++}`
58
- }
59
- usedNames.add(name)
60
- return name
61
- },
62
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
63
- })
64
- }
65
-
66
20
  /**
67
21
  * Reads the server URL from the document's `servers` array at `serverIndex`,
68
22
  * interpolating any `serverVariables` into the URL template.
@@ -163,7 +117,34 @@ export function preScan({
163
117
  if (operationNode) operationNodes.push(operationNode)
164
118
  }
165
119
 
166
- dedupePlan = createDedupePlan({ schemaNodes: allNodes, operationNodes, schemaNames: Object.keys(schemas), circularNames })
120
+ // Only enums and objects are candidates, and object shapes that reference a circular schema are
121
+ // rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
122
+ // name (collision-resolved against existing component names). Shapes without a name stay inline.
123
+ const circularSchemas = new Set(circularNames)
124
+ const usedNames = new Set(Object.keys(schemas))
125
+
126
+ dedupePlan = ast.buildDedupePlan([...allNodes, ...operationNodes], {
127
+ isCandidate: (node) => {
128
+ if (node.type === 'enum') return true
129
+ if (node.type !== 'object') return false
130
+ // Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
131
+ if (node.name && circularSchemas.has(node.name)) return false
132
+ return !containsCircularRef(node, { circularSchemas })
133
+ },
134
+ nameFor: (node) => {
135
+ const base = node.name
136
+ if (!base) return null
137
+
138
+ let name = base
139
+ let counter = 2
140
+ while (usedNames.has(name)) {
141
+ name = `${base}${counter++}`
142
+ }
143
+ usedNames.add(name)
144
+ return name
145
+ },
146
+ refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
147
+ })
167
148
 
168
149
  for (const definition of dedupePlan.hoisted) {
169
150
  if (definition.type === 'enum' && definition.name) enumNames.push(definition.name)