@kubb/adapter-oas 5.0.0-beta.29 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-beta.29",
3
+ "version": "5.0.0-beta.30",
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",
@@ -47,7 +47,7 @@
47
47
  "oas": "^32.1.18",
48
48
  "oas-normalize": "^16.0.4",
49
49
  "swagger2openapi": "^7.0.8",
50
- "@kubb/core": "5.0.0-beta.29"
50
+ "@kubb/core": "5.0.0-beta.30"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
package/src/dialect.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { ast } from '@kubb/core'
2
+ import { isDiscriminator, isNullable, isReference } from './guards.ts'
3
+ import { resolveRef } from './refs.ts'
4
+ import type { SchemaObject } from './types.ts'
5
+
6
+ /**
7
+ * The OpenAPI / Swagger dialect — the default used by `@kubb/adapter-oas`.
8
+ *
9
+ * Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
10
+ * decisions that differ between specs (nullability, `$ref`, discriminator, binary,
11
+ * ref resolution) so the converter pipeline and dispatch rules stay shared. A
12
+ * future adapter (e.g. AsyncAPI) ships its own dialect — `type: ['null', …]`
13
+ * nullability, no discriminator object, binary via `contentEncoding` — and reuses
14
+ * the rest unchanged.
15
+ *
16
+ * Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
17
+ * JSON Schema vocabulary, so the converters keep that common case.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const parser = createSchemaParser(context) // uses oasDialect
22
+ * const parser = createSchemaParser(context, oasDialect) // explicit
23
+ * ```
24
+ */
25
+ export const oasDialect = ast.defineSchemaDialect({
26
+ name: 'oas',
27
+ isNullable,
28
+ isReference,
29
+ isDiscriminator,
30
+ isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
31
+ resolveRef,
32
+ })
33
+
34
+ /**
35
+ * The concrete dialect type for `@kubb/adapter-oas`. Keeps the OAS guard predicates
36
+ * (`isReference`, `isDiscriminator`) intact so converters narrow schemas after a check.
37
+ */
38
+ export type OasDialect = typeof oasDialect
package/src/parser.ts CHANGED
@@ -2,8 +2,7 @@ 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,
@@ -60,6 +59,13 @@ type SchemaContext = {
60
59
  options: ast.ParserOptions
61
60
  }
62
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
+
63
69
  /**
64
70
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
65
71
  *
@@ -88,7 +94,7 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
88
94
  *
89
95
  * @internal
90
96
  */
91
- export function createSchemaParser(ctx: OasParserContext) {
97
+ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
92
98
  const document = ctx.document
93
99
 
94
100
  // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
@@ -127,7 +133,7 @@ export function createSchemaParser(ctx: OasParserContext) {
127
133
  if (refPath && !resolvingRefs.has(refPath)) {
128
134
  if (!resolvedRefCache.has(refPath)) {
129
135
  try {
130
- const referenced = resolveRef<SchemaObject>(document, refPath)
136
+ const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
131
137
  if (referenced) {
132
138
  resolvingRefs.add(refPath)
133
139
  resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
@@ -188,13 +194,13 @@ export function createSchemaParser(ctx: OasParserContext) {
188
194
  }> = []
189
195
  const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
190
196
  .filter((item) => {
191
- if (!isReference(item) || !name) return true
192
- const deref = resolveRef<SchemaObject>(document, item.$ref)
193
- 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
194
200
  const parentUnion = deref.oneOf ?? deref.anyOf
195
201
  if (!parentUnion) return true
196
202
  const childRef = `${SCHEMA_REF_PREFIX}${name}`
197
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
203
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
198
204
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
199
205
  if (inOneOf || inMapping) {
200
206
  const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef)
@@ -218,9 +224,9 @@ export function createSchemaParser(ctx: OasParserContext) {
218
224
 
219
225
  if (missingRequired.length) {
220
226
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
221
- if (!isReference(item)) return [item as SchemaObject]
222
- const deref = resolveRef<SchemaObject>(document, item.$ref)
223
- 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] : []
224
230
  })
225
231
 
226
232
  for (const key of missingRequired) {
@@ -287,10 +293,10 @@ export function createSchemaParser(ctx: OasParserContext) {
287
293
  const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
288
294
  const unionBase = {
289
295
  ...buildSchemaNode(schema, name, nullable, defaultValue),
290
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
296
+ discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
291
297
  strategy,
292
298
  }
293
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
299
+ const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
294
300
  const sharedPropertiesNode = schema.properties
295
301
  ? (() => {
296
302
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
@@ -303,7 +309,7 @@ export function createSchemaParser(ctx: OasParserContext) {
303
309
 
304
310
  if (sharedPropertiesNode || discriminator?.mapping) {
305
311
  const members = unionMembers.map((s) => {
306
- const ref = isReference(s) ? s.$ref : undefined
312
+ const ref = dialect.isReference(s) ? s.$ref : undefined
307
313
  const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref)
308
314
  const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
309
315
 
@@ -548,7 +554,7 @@ export function createSchemaParser(ctx: OasParserContext) {
548
554
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
549
555
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
550
556
  const resolvedPropSchema = propSchema as SchemaObject
551
- const propNullable = isNullable(resolvedPropSchema)
557
+ const propNullable = dialect.isNullable(resolvedPropSchema)
552
558
 
553
559
  const resolvedChildName = ast.childName(name, propName)
554
560
  const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
@@ -612,7 +618,7 @@ export function createSchemaParser(ctx: OasParserContext) {
612
618
  ...buildSchemaNode(schema, name, nullable, defaultValue),
613
619
  })
614
620
 
615
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
621
+ if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
616
622
  const discPropName = schema.discriminator.propertyName
617
623
  const values = Object.keys(schema.discriminator.mapping)
618
624
  const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
@@ -721,12 +727,86 @@ export function createSchemaParser(ctx: OasParserContext) {
721
727
  })
722
728
  }
723
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),
739
+ })
740
+ }
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
+
724
804
  /**
725
805
  * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
726
806
  *
727
- * Dispatch order (first match wins): `$ref` `allOf` `oneOf`/`anyOf` `const` → `format`
728
- * octet-stream blob multi-type array constraint-inferred type `enum` object/array/tuple/scalar
729
- * → 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`.
730
810
  */
731
811
  function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
732
812
  const options: ast.ParserOptions = {
@@ -738,11 +818,11 @@ export function createSchemaParser(ctx: OasParserContext) {
738
818
  return parseSchema({ schema: flattenedSchema, name }, rawOptions)
739
819
  }
740
820
 
741
- const nullable = isNullable(schema) || undefined
821
+ const nullable = dialect.isNullable(schema) || undefined
742
822
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
743
823
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
744
824
 
745
- const ctx: SchemaContext = {
825
+ const schemaCtx: SchemaContext = {
746
826
  schema,
747
827
  name,
748
828
  nullable,
@@ -752,58 +832,8 @@ export function createSchemaParser(ctx: OasParserContext) {
752
832
  options,
753
833
  }
754
834
 
755
- if (isReference(schema)) return convertRef(ctx)
756
-
757
- if (schema.allOf?.length) return convertAllOf(ctx)
758
- const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
759
- if (unionMembers.length) return convertUnion(ctx)
760
-
761
- if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
762
-
763
- if (schema.format) {
764
- const formatResult = convertFormat(ctx)
765
- if (formatResult) return formatResult
766
- }
767
-
768
- if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
769
- return ast.createSchema({
770
- type: 'blob',
771
- primitive: 'string',
772
- ...buildSchemaNode(schema, name, nullable, defaultValue),
773
- })
774
- }
775
-
776
- if (Array.isArray(schema.type) && schema.type.length > 1) {
777
- const nonNullTypes = schema.type.filter((t) => t !== 'null') as Array<string>
778
- const arrayNullable = schema.type.includes('null') || nullable || undefined
779
-
780
- if (nonNullTypes.length > 1) {
781
- return ast.createSchema({
782
- type: 'union',
783
- members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
784
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
785
- })
786
- }
787
- }
788
-
789
- if (!type) {
790
- if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
791
- return convertString(ctx)
792
- }
793
- if (schema.minimum !== undefined || schema.maximum !== undefined) {
794
- return convertNumeric(ctx, 'number')
795
- }
796
- }
797
-
798
- if (schema.enum?.length) return convertEnum(ctx)
799
- if (type === 'object' || schema.properties || schema.additionalProperties || 'patternProperties' in schema) return convertObject(ctx)
800
- if ('prefixItems' in schema) return convertTuple(ctx)
801
- if (type === 'array' || 'items' in schema) return convertArray(ctx)
802
- if (type === 'string') return convertString(ctx)
803
- if (type === 'number') return convertNumeric(ctx, 'number')
804
- if (type === 'integer') return convertNumeric(ctx, 'integer')
805
- if (type === 'boolean') return convertBoolean(ctx)
806
- if (type === 'null') return convertNull(ctx)
835
+ const node = ast.dispatch(schemaRules, schemaCtx)
836
+ if (node) return node
807
837
 
808
838
  const emptyType = typeOptionMap.get(options.emptySchemaType)!
809
839
  return ast.createSchema({
@@ -883,7 +913,7 @@ export function createSchemaParser(ctx: OasParserContext) {
883
913
  const keys: Array<string> = []
884
914
  for (const key in schema.properties) {
885
915
  const prop = schema.properties[key]
886
- if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
916
+ if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
887
917
  keys.push(key)
888
918
  }
889
919
  }
@@ -969,6 +999,7 @@ export function createSchemaParser(ctx: OasParserContext) {
969
999
 
970
1000
  return ast.createOperation({
971
1001
  operationId,
1002
+ protocol: 'http',
972
1003
  method: operation.method.toUpperCase() as ast.HttpMethod,
973
1004
  path: urlPath.path,
974
1005
  tags: operation.getTags().map((tag) => tag.name),