@kubb/adapter-oas 5.0.0-alpha.9 → 5.0.0-beta.10

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/constants.ts CHANGED
@@ -1,41 +1,83 @@
1
- import type { SchemaType } from '@kubb/ast/types'
2
- import type { HttpMethods as OASHttpMethods } from 'oas/types'
1
+ import { ast } from '@kubb/core'
3
2
 
4
- // ─── Merge defaults ────────────────────────────────────────────────────────────
3
+ /**
4
+ * Default parser options applied when no explicit options are provided.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
9
+ *
10
+ * const parser = createOasParser(oas)
11
+ * const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
12
+ * ```
13
+ */
14
+ export const DEFAULT_PARSER_OPTIONS = {
15
+ dateType: 'string',
16
+ integerType: 'bigint',
17
+ unknownType: 'any',
18
+ emptySchemaType: 'any',
19
+ enumSuffix: 'enum',
20
+ } as const satisfies ast.ParserOptions
21
+
22
+ /**
23
+ * JSON-Pointer prefix for schemas declared under `components.schemas` in an OpenAPI document.
24
+ *
25
+ * Used when building or parsing `$ref` strings.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * `${SCHEMA_REF_PREFIX}Pet` // '#/components/schemas/Pet'
30
+ * ```
31
+ */
32
+ export const SCHEMA_REF_PREFIX = '#/components/schemas/' as const
5
33
 
6
34
  /**
7
- * OpenAPI version string written into merged document stubs.
35
+ * OpenAPI version string written into the stub document created during multi-spec merges.
8
36
  */
9
37
  export const MERGE_OPENAPI_VERSION = '3.0.0' as const
10
38
 
11
39
  /**
12
- * Fallback `info.title` used when merging multiple API documents.
40
+ * Fallback `info.title` placed in the stub document when merging multiple API files.
13
41
  */
14
42
  export const MERGE_DEFAULT_TITLE = 'Merged API' as const
15
43
 
16
44
  /**
17
- * Fallback `info.version` used when merging multiple API documents.
45
+ * Fallback `info.version` placed in the stub document when merging multiple API files.
18
46
  */
19
47
  export const MERGE_DEFAULT_VERSION = '1.0.0' as const
20
48
 
21
- // ─── Schema analysis ───────────────────────────────────────────────────────────
22
-
23
49
  /**
24
- * JSON Schema keywords that indicate structural composition.
25
- * A schema fragment containing any of these keys must not be inlined into its
26
- * parent during `allOf` flattening it carries semantic meaning of its own.
50
+ * Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
51
+ *
52
+ * A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
53
+ * intersection member rather than being merged into the parent.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { structuralKeys } from '@kubb/adapter-oas'
58
+ *
59
+ * const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
60
+ * // true when fragment has e.g. 'properties' or 'oneOf'
61
+ * ```
27
62
  */
28
63
  export const structuralKeys = new Set(['properties', 'items', 'additionalProperties', 'oneOf', 'anyOf', 'allOf', 'not'] as const)
29
64
 
30
65
  /**
31
- * Maps OAS/JSON Schema `format` strings to their Kubb `SchemaType` equivalents.
66
+ * Static map from OAS `format` strings to Kubb `SchemaType` values.
67
+ *
68
+ * Only formats whose AST type differs from the OAS `type` field appear here.
69
+ * Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled separately
70
+ * in the parser. `ipv4` and `ipv6` map to their own dedicated schema types; `hostname` and
71
+ * `idn-hostname` map to `'url'` as the closest generic string-format type.
32
72
  *
33
- * Only formats that need a type different from the raw OAS `type` are listed.
34
- * `int64`, `date-time`, `date`, and `time` are handled separately because their
35
- * mapping depends on runtime parser options.
73
+ * @example
74
+ * ```ts
75
+ * import { formatMap } from '@kubb/adapter-oas'
36
76
  *
37
- * Note: `ipv4`, `ipv6`, and `hostname` map to `'url'` — the closest supported
38
- * scalar type in the Kubb AST, even though these are not strictly URLs.
77
+ * formatMap['uuid'] // 'uuid'
78
+ * formatMap['binary'] // 'blob'
79
+ * formatMap['float'] // 'number'
80
+ * ```
39
81
  */
40
82
  export const formatMap = {
41
83
  uuid: 'uuid',
@@ -44,8 +86,8 @@ export const formatMap = {
44
86
  uri: 'url',
45
87
  'uri-reference': 'url',
46
88
  url: 'url',
47
- ipv4: 'url',
48
- ipv6: 'url',
89
+ ipv4: 'ipv4',
90
+ ipv6: 'ipv6',
49
91
  hostname: 'url',
50
92
  'idn-hostname': 'url',
51
93
  binary: 'blob',
@@ -55,60 +97,26 @@ export const formatMap = {
55
97
  int32: 'integer',
56
98
  float: 'number',
57
99
  double: 'number',
58
- } as const satisfies Record<string, SchemaType>
59
-
60
- /**
61
- * Exhaustive list of media types that Kubb recognizes.
62
- */
63
- export const knownMediaTypes = new Set([
64
- 'application/json',
65
- 'application/xml',
66
- 'application/x-www-form-urlencoded',
67
- 'application/octet-stream',
68
- 'application/pdf',
69
- 'application/zip',
70
- 'application/graphql',
71
- 'multipart/form-data',
72
- 'text/plain',
73
- 'text/html',
74
- 'text/csv',
75
- 'text/xml',
76
- 'image/png',
77
- 'image/jpeg',
78
- 'image/gif',
79
- 'image/webp',
80
- 'image/svg+xml',
81
- 'audio/mpeg',
82
- 'video/mp4',
83
- ] as const)
100
+ } as const satisfies Record<string, ast.SchemaType>
84
101
 
85
102
  /**
86
- * Vendor extension keys used to attach human-readable labels to enum values.
87
- * Checked in priority order: the first key found wins.
103
+ * Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { enumExtensionKeys } from '@kubb/adapter-oas'
108
+ *
109
+ * const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
110
+ * ```
88
111
  */
89
112
  export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
90
113
 
91
114
  /**
92
- * Scalar primitive schema types used for union member simplification.
93
- */
94
- export const SCALAR_PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'bigint', 'boolean'] as const)
95
-
96
- // ─── HTTP ──────────────────────────────────────────────────────────────────────
97
-
98
- /**
99
- * Canonical HTTP method names for the Kubb OAS layer.
100
- * Keys are uppercase (used in generated code); values are the lowercase strings
101
- * that the `oas` library uses internally.
102
- *
103
- * TODO(v5): remove — use `httpMethods` from `@kubb/ast`
115
+ * Maps `'any' | 'unknown' | 'void'` option strings to their `ScalarSchemaType` constant.
116
+ * Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
104
117
  */
105
- export const httpMethods = {
106
- GET: 'get',
107
- POST: 'post',
108
- PUT: 'put',
109
- PATCH: 'patch',
110
- DELETE: 'delete',
111
- HEAD: 'head',
112
- OPTIONS: 'options',
113
- TRACE: 'trace',
114
- } as const satisfies Record<Uppercase<OASHttpMethods>, OASHttpMethods>
118
+ export const typeOptionMap = new Map<'any' | 'unknown' | 'void', ast.ScalarSchemaType>([
119
+ ['any', ast.schemaTypes.any],
120
+ ['unknown', ast.schemaTypes.unknown],
121
+ ['void', ast.schemaTypes.void],
122
+ ])
@@ -0,0 +1,108 @@
1
+ import { ast } from '@kubb/core'
2
+
3
+ type DiscriminatorTarget = {
4
+ propertyName: string
5
+ enumValues: Array<string | number | boolean>
6
+ }
7
+
8
+ /**
9
+ * Injects discriminator enum values into child schemas so they know which value identifies them.
10
+ *
11
+ * Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
12
+ * enum value each union member is mapped to, then adds (or replaces) that property on the matching
13
+ * child object schema.
14
+ *
15
+ * Returns a new `InputNode` — the original is never mutated.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const { root } = parseOas(document, options)
20
+ * const next = applyDiscriminatorInheritance(root)
21
+ * ```
22
+ */
23
+ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
24
+ const childMap = new Map<string, DiscriminatorTarget>()
25
+
26
+ for (const schema of root.schemas) {
27
+ // Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
28
+ // Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
29
+ let unionNode = ast.narrowSchema(schema, 'union')
30
+
31
+ if (!unionNode) {
32
+ const intersectionMembers = ast.narrowSchema(schema, 'intersection')?.members
33
+ if (intersectionMembers) {
34
+ for (const m of intersectionMembers) {
35
+ const u = ast.narrowSchema(m, 'union')
36
+ if (u) {
37
+ unionNode = u
38
+ break
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
45
+
46
+ const { discriminatorPropertyName, members } = unionNode
47
+
48
+ for (const member of members) {
49
+ // Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
50
+ const intersectionNode = ast.narrowSchema(member, 'intersection')
51
+ if (!intersectionNode?.members) continue
52
+
53
+ let refNode: ReturnType<typeof ast.narrowSchema<'ref'>> | undefined
54
+ let objNode: ReturnType<typeof ast.narrowSchema<'object'>> | undefined
55
+
56
+ for (const m of intersectionNode.members) {
57
+ refNode ??= ast.narrowSchema(m, 'ref')
58
+ objNode ??= ast.narrowSchema(m, 'object')
59
+ }
60
+
61
+ if (!refNode?.name || !objNode) continue
62
+
63
+ const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
64
+ const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : undefined
65
+ if (!enumNode?.enumValues?.length) continue
66
+
67
+ const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
68
+ if (!enumValues.length) continue
69
+
70
+ const existing = childMap.get(refNode.name)
71
+ if (existing) {
72
+ existing.enumValues.push(...enumValues)
73
+ } else {
74
+ childMap.set(refNode.name, {
75
+ propertyName: discriminatorPropertyName,
76
+ enumValues: [...enumValues],
77
+ })
78
+ }
79
+ }
80
+ }
81
+
82
+ if (childMap.size === 0) return root
83
+
84
+ return ast.transform(root, {
85
+ schema(node, { parent }) {
86
+ if (parent?.kind !== 'Input' || !node.name) return
87
+
88
+ const entry = childMap.get(node.name)
89
+ if (!entry) return
90
+
91
+ const objectNode = ast.narrowSchema(node, 'object')
92
+ if (!objectNode) return
93
+
94
+ const { propertyName, enumValues } = entry
95
+ const enumSchema = ast.createSchema({ type: 'enum', enumValues })
96
+ const newProp = ast.createProperty({
97
+ name: propertyName,
98
+ required: true,
99
+ schema: enumSchema,
100
+ })
101
+
102
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
103
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
104
+
105
+ return { ...objectNode, properties: newProperties }
106
+ },
107
+ })
108
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,162 @@
1
+ import path from 'node:path'
2
+ import { mergeDeep, URLPath } from '@internals/utils'
3
+ import type { AdapterSource } from '@kubb/core'
4
+ import { bundle, loadConfig } from '@redocly/openapi-core'
5
+ import OASNormalize from 'oas-normalize'
6
+ import swagger2openapi from 'swagger2openapi'
7
+ import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
8
+ import { isOpenApiV2Document } from './guards.ts'
9
+ import type { Document } from './types.ts'
10
+
11
+ export type ParseOptions = {
12
+ canBundle?: boolean
13
+ enablePaths?: boolean
14
+ }
15
+
16
+ export type ValidateDocumentOptions = {
17
+ throwOnError?: boolean
18
+ }
19
+
20
+ /**
21
+ * Loads and dereferences an OpenAPI document, returning the raw `Document`.
22
+ *
23
+ * Accepts a file path string or an already-parsed document object. File paths are bundled via
24
+ * Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
25
+ * to OpenAPI 3.0 via `swagger2openapi`.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const document = await parseDocument('./openapi.yaml')
30
+ * const document = await parse(rawDocumentObject, { canBundle: false })
31
+ * ```
32
+ */
33
+ export async function parseDocument(pathOrApi: string | Document, { canBundle = true, enablePaths = true }: ParseOptions = {}): Promise<Document> {
34
+ if (typeof pathOrApi === 'string' && canBundle) {
35
+ const config = await loadConfig()
36
+ const bundleResults = await bundle({
37
+ ref: pathOrApi,
38
+ config,
39
+ base: pathOrApi,
40
+ })
41
+
42
+ return parseDocument(bundleResults.bundle.parsed as string, {
43
+ canBundle,
44
+ enablePaths,
45
+ })
46
+ }
47
+
48
+ const oasNormalize = new OASNormalize(pathOrApi, {
49
+ enablePaths,
50
+ colorizeErrors: true,
51
+ })
52
+ const document = (await oasNormalize.load()) as Document
53
+
54
+ if (isOpenApiV2Document(document)) {
55
+ const { openapi } = await swagger2openapi.convertObj(document, {
56
+ anchors: true,
57
+ })
58
+
59
+ return openapi as Document
60
+ }
61
+
62
+ return document
63
+ }
64
+
65
+ /**
66
+ * Deep-merges multiple OpenAPI documents into a single `Document`.
67
+ *
68
+ * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
69
+ * Throws when the input array is empty.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
74
+ * ```
75
+ */
76
+ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
77
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
78
+
79
+ if (documents.length === 0) {
80
+ throw new Error('No OAS documents provided for merging.')
81
+ }
82
+
83
+ const seed: Document = {
84
+ openapi: MERGE_OPENAPI_VERSION,
85
+ info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
86
+ paths: {},
87
+ components: { schemas: {} },
88
+ } as Document
89
+
90
+ const merged = documents.reduce(
91
+ (acc, current) => mergeDeep(acc as Record<string, unknown>, current as Record<string, unknown>),
92
+ seed as Record<string, unknown>,
93
+ )
94
+
95
+ return parseDocument(merged as Document)
96
+ }
97
+
98
+ /**
99
+ * Creates a `Document` from an `AdapterSource`.
100
+ *
101
+ * Handles all three source types:
102
+ * - `{ type: 'path' }` — resolves and bundles a local file path or remote URL.
103
+ * - `{ type: 'paths' }` — merges multiple file paths into a single document.
104
+ * - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
109
+ * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
110
+ * ```
111
+ */
112
+ export function parseFromConfig(source: AdapterSource): Promise<Document> {
113
+ if (source.type === 'data') {
114
+ if (typeof source.data === 'object') {
115
+ return parseDocument(structuredClone(source.data) as Document)
116
+ }
117
+
118
+ return parseDocument(source.data as string, { canBundle: false })
119
+ }
120
+
121
+ if (source.type === 'paths') {
122
+ return mergeDocuments(source.paths)
123
+ }
124
+
125
+ // type === 'path'
126
+ if (new URLPath(source.path).isURL) {
127
+ return parseDocument(source.path)
128
+ }
129
+
130
+ return parseDocument(path.resolve(path.dirname(source.path), source.path))
131
+ }
132
+
133
+ /**
134
+ * Validates an OpenAPI document using `oas-normalize` with colorized error output.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * await validateDocument(document)
139
+ * ```
140
+ */
141
+ export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
142
+ try {
143
+ const oasNormalize = new OASNormalize(document, {
144
+ enablePaths: true,
145
+ colorizeErrors: true,
146
+ })
147
+
148
+ await oasNormalize.validate({
149
+ parser: {
150
+ validate: {
151
+ errors: { colorize: true },
152
+ },
153
+ },
154
+ })
155
+ } catch (error) {
156
+ if (throwOnError) {
157
+ throw error
158
+ }
159
+
160
+ // Validation failures are non-fatal — mirror plugin-oas behavior
161
+ }
162
+ }
package/src/guards.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { isPlainObject } from '@internals/utils'
2
+ import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
3
+ import type { DiscriminatorObject, SchemaObject } from './types.ts'
4
+
5
+ /**
6
+ * Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * if (isOpenApiV2Document(doc)) {
11
+ * // doc is OpenAPIV2.Document
12
+ * }
13
+ * ```
14
+ */
15
+ export function isOpenApiV2Document(doc: unknown): doc is OpenAPIV2.Document {
16
+ return !!doc && isPlainObject(doc) && !('openapi' in doc)
17
+ }
18
+
19
+ /**
20
+ * Returns `true` when a schema should be treated as nullable.
21
+ *
22
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
23
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * isNullable({ type: 'string', nullable: true }) // true
28
+ * isNullable({ type: ['string', 'null'] }) // true
29
+ * isNullable({ type: 'string' }) // false
30
+ * ```
31
+ */
32
+ export function isNullable(schema?: SchemaObject & { 'x-nullable'?: boolean }): boolean {
33
+ const explicitNullable = schema?.nullable ?? schema?.['x-nullable']
34
+ if (explicitNullable === true) return true
35
+
36
+ const schemaType = schema?.type
37
+ if (schemaType === 'null') return true
38
+ if (Array.isArray(schemaType)) return schemaType.includes('null')
39
+
40
+ return false
41
+ }
42
+
43
+ /**
44
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * isReference({ $ref: '#/components/schemas/Pet' }) // true
49
+ * isReference({ type: 'string' }) // false
50
+ * ```
51
+ */
52
+ export function isReference(obj?: unknown): obj is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
53
+ return !!obj && typeof obj === 'object' && '$ref' in obj
54
+ }
55
+
56
+ /**
57
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
62
+ * isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
63
+ * ```
64
+ */
65
+ export function isDiscriminator(obj?: unknown): obj is SchemaObject & { discriminator: DiscriminatorObject } {
66
+ const record = obj as Record<string, unknown>
67
+ return !!obj && !!record['discriminator'] && typeof record['discriminator'] !== 'string'
68
+ }
package/src/index.ts CHANGED
@@ -1 +1,18 @@
1
1
  export { adapterOas, adapterOasName } from './adapter.ts'
2
+ export type { ValidateDocumentOptions } from './factory.ts'
3
+ export { mergeDocuments } from './factory.ts'
4
+ export type {
5
+ AdapterOas,
6
+ AdapterOasOptions,
7
+ AdapterOasResolvedOptions,
8
+ ContentType,
9
+ DiscriminatorObject,
10
+ Document,
11
+ HttpMethod,
12
+ MediaTypeObject,
13
+ Operation,
14
+ ReferenceObject,
15
+ ResponseObject,
16
+ SchemaObject,
17
+ } from './types.ts'
18
+ export { HttpMethods } from './types.ts'