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

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.
@@ -1,132 +0,0 @@
1
- import { ast } from '@kubb/core'
2
- import type { SchemaNodeByType } from '@kubb/ast'
3
-
4
- export type DiscriminatorTarget = {
5
- propertyName: string
6
- enumValues: Array<string | number | boolean>
7
- }
8
-
9
- /**
10
- * Builds a map of child schema names → discriminator patch data by scanning the given
11
- * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
12
- *
13
- * The streaming path calls this on a small pre-parsed subset of schemas (only the
14
- * discriminator parents) rather than on all schemas at once.
15
- */
16
- export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
17
- const childMap = new Map<string, DiscriminatorTarget>()
18
-
19
- for (const schema of schemas) {
20
- // Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
21
- // Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
22
- let unionNode = ast.narrowSchema(schema, 'union')
23
-
24
- if (!unionNode) {
25
- const intersectionMembers = ast.narrowSchema(schema, 'intersection')?.members
26
- if (intersectionMembers) {
27
- for (const m of intersectionMembers) {
28
- const u = ast.narrowSchema(m, 'union')
29
- if (u) {
30
- unionNode = u
31
- break
32
- }
33
- }
34
- }
35
- }
36
-
37
- if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
38
-
39
- const { discriminatorPropertyName, members } = unionNode
40
-
41
- for (const member of members) {
42
- // Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
43
- const intersectionNode = ast.narrowSchema(member, 'intersection')
44
- if (!intersectionNode?.members) continue
45
-
46
- let refNode: SchemaNodeByType['ref'] | null = null
47
- let objNode: SchemaNodeByType['object'] | null = null
48
-
49
- for (const m of intersectionNode.members) {
50
- refNode ??= ast.narrowSchema(m, 'ref')
51
- objNode ??= ast.narrowSchema(m, 'object')
52
- }
53
-
54
- if (!refNode?.name || !objNode) continue
55
-
56
- const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
57
- const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
58
- if (!enumNode?.enumValues?.length) continue
59
-
60
- const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
61
- if (!enumValues.length) continue
62
-
63
- const existing = childMap.get(refNode.name)
64
- if (!existing) {
65
- childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
66
- continue
67
- }
68
- existing.enumValues.push(...enumValues)
69
- }
70
- }
71
-
72
- return childMap
73
- }
74
-
75
- /**
76
- * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
77
- * the discriminant property). Used by the streaming path to apply patches inline per yield
78
- * without buffering all schemas.
79
- */
80
- export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
81
- const objectNode = ast.narrowSchema(node, 'object')
82
- if (!objectNode) return node
83
-
84
- const { propertyName, enumValues } = entry
85
- const enumSchema = ast.factory.createSchema({ type: 'enum', enumValues })
86
- const newProp = ast.factory.createProperty({ name: propertyName, required: true, schema: enumSchema })
87
-
88
- const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
89
- const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
90
-
91
- return { ...objectNode, properties: newProperties }
92
- }
93
-
94
- /**
95
- * Creates a single-property object schema used as a discriminator literal.
96
- *
97
- * @example
98
- * ```ts
99
- * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
100
- * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
101
- * ```
102
- */
103
- export function createDiscriminantNode({ propertyName, value }: { propertyName: string; value: string }): ast.SchemaNode {
104
- return ast.factory.createSchema({
105
- type: 'object',
106
- primitive: 'object',
107
- properties: [
108
- ast.factory.createProperty({
109
- name: propertyName,
110
- schema: ast.factory.createSchema({
111
- type: 'enum',
112
- primitive: 'string',
113
- enumValues: [value],
114
- }),
115
- required: true,
116
- }),
117
- ],
118
- })
119
- }
120
-
121
- /**
122
- * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
123
- *
124
- * @example
125
- * ```ts
126
- * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
127
- * ```
128
- */
129
- export function findDiscriminator(mapping: Record<string, string> | undefined, ref: string | undefined): string | null {
130
- if (!mapping || !ref) return null
131
- return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null
132
- }
package/src/factory.ts DELETED
@@ -1,180 +0,0 @@
1
- import path from 'node:path'
2
- import { exists, mergeDeep, Url } from '@internals/utils'
3
- import { Diagnostics } from '@kubb/core'
4
- import type { AdapterSource } from '@kubb/core'
5
- import { compileErrors, validate } from '@readme/openapi-parser'
6
- import { parse } from 'yaml'
7
- import { bundleDocument } from './bundler.ts'
8
- import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
9
- import { isOpenApiV2Document } from './guards.ts'
10
- import type { Document } from './types.ts'
11
-
12
- export type ParseOptions = {
13
- canBundle?: 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 and URLs are
24
- * bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
25
- * entries so generators can emit named types and imports. Swagger 2.0 documents are
26
- * automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
27
- *
28
- * @example
29
- * ```ts
30
- * const document = await parseDocument('./openapi.yaml')
31
- * const document = await parse(rawDocumentObject, { canBundle: false })
32
- * ```
33
- */
34
- export async function parseDocument(pathOrApi: string | Document, { canBundle = true }: ParseOptions = {}): Promise<Document> {
35
- if (typeof pathOrApi === 'string' && canBundle) {
36
- const bundled = await bundleDocument(pathOrApi)
37
-
38
- return parseDocument(bundled, { canBundle: false })
39
- }
40
-
41
- // A string here is always inline YAML/JSON content: file paths and URLs are read and parsed by
42
- // `bundleDocument` first. `yaml.parse` also parses JSON, since JSON is a subset of YAML.
43
- const document = (typeof pathOrApi === 'string' ? parse(pathOrApi) : pathOrApi) as Document
44
-
45
- if (isOpenApiV2Document(document)) {
46
- const { default: swagger2openapi } = await import('swagger2openapi')
47
- const { openapi } = await swagger2openapi.convertObj(document, {
48
- anchors: true,
49
- })
50
-
51
- return openapi as Document
52
- }
53
-
54
- return document
55
- }
56
-
57
- /**
58
- * Deep-merges multiple OpenAPI documents into a single `Document`.
59
- *
60
- * Each document is parsed independently, then deep-merged into one in array order.
61
- * Throws when the input array is empty.
62
- *
63
- * @example
64
- * ```ts
65
- * const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
66
- * ```
67
- */
68
- export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
69
- const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })))
70
-
71
- if (documents.length === 0) {
72
- throw new Diagnostics.Error({
73
- code: Diagnostics.code.inputRequired,
74
- severity: 'error',
75
- message: 'No OAS documents were provided for merging.',
76
- help: 'Pass at least one path or document to `input.path`.',
77
- location: { kind: 'config' },
78
- })
79
- }
80
-
81
- const seed: Document = {
82
- openapi: MERGE_OPENAPI_VERSION,
83
- info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
84
- paths: {},
85
- components: { schemas: {} },
86
- } as Document
87
-
88
- const merged = documents.reduce(
89
- (acc, current) => mergeDeep(acc as Record<string, unknown>, current as Record<string, unknown>),
90
- seed as Record<string, unknown>,
91
- )
92
-
93
- return parseDocument(merged as Document)
94
- }
95
-
96
- /**
97
- * Creates a `Document` from an `AdapterSource`.
98
- *
99
- * Handles all three source types:
100
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
101
- * - `{ type: 'paths' }` merges multiple file paths into a single document.
102
- * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
103
- *
104
- * @example
105
- * ```ts
106
- * const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
107
- * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
108
- * ```
109
- */
110
- export async function parseFromConfig(source: AdapterSource): Promise<Document> {
111
- if (source.type === 'data') {
112
- if (typeof source.data === 'object') {
113
- return parseDocument(structuredClone(source.data) as Document)
114
- }
115
-
116
- return parseDocument(source.data as string, { canBundle: false })
117
- }
118
-
119
- if (source.type === 'paths') {
120
- return mergeDocuments(source.paths)
121
- }
122
-
123
- // type === 'path'
124
- if (Url.canParse(source.path)) {
125
- return parseDocument(source.path)
126
- }
127
-
128
- const resolved = path.resolve(path.dirname(source.path), source.path)
129
- await assertInputExists(resolved)
130
- return parseDocument(resolved)
131
- }
132
-
133
- /**
134
- * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
135
- * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
136
- * its parse error instead.
137
- */
138
- export async function assertInputExists(input: string): Promise<void> {
139
- if (Url.canParse(input)) {
140
- return
141
- }
142
- if (!(await exists(input))) {
143
- throw new Diagnostics.Error({
144
- code: Diagnostics.code.inputNotFound,
145
- severity: 'error',
146
- message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
147
- help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
148
- location: { kind: 'config' },
149
- })
150
- }
151
- }
152
-
153
- /**
154
- * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
155
- *
156
- * @example
157
- * ```ts
158
- * await validateDocument(document)
159
- * ```
160
- */
161
- export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
162
- try {
163
- // `validate` dereferences its input in place, so clone to keep the cached document intact.
164
- const result = await validate(structuredClone(document), {
165
- validate: {
166
- errors: { colorize: true },
167
- },
168
- })
169
-
170
- if (!result.valid) {
171
- throw new Error(compileErrors(result))
172
- }
173
- } catch (error) {
174
- if (throwOnError) {
175
- throw error
176
- }
177
-
178
- // Validation failures are non-fatal, mirror plugin-oas behavior
179
- }
180
- }
package/src/guards.ts DELETED
@@ -1,68 +0,0 @@
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 DELETED
@@ -1,15 +0,0 @@
1
- export { adapterOas, adapterOasName } from './adapter.ts'
2
- export type {
3
- AdapterOas,
4
- AdapterOasOptions,
5
- AdapterOasResolvedOptions,
6
- ContentType,
7
- DiscriminatorObject,
8
- Document,
9
- HttpMethod,
10
- MediaTypeObject,
11
- Operation,
12
- ReferenceObject,
13
- ResponseObject,
14
- SchemaObject,
15
- } from './types.ts'
package/src/mime.ts DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * MIME type fragments that mark a media type as JSON-like.
3
- *
4
- * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
5
- * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
6
- * `application/vnd.api+json`.
7
- */
8
- const jsonMimeFragments = ['application/json', 'application/x-json', 'text/json', 'text/x-json', '+json'] as const
9
-
10
- /**
11
- * Returns `true` when a media type string is JSON-like.
12
- *
13
- * @example
14
- * ```ts
15
- * isJsonMimeType('application/json') // true
16
- * isJsonMimeType('application/vnd.api+json') // true
17
- * isJsonMimeType('multipart/form-data') // false
18
- * ```
19
- */
20
- export function isJsonMimeType(mimeType: string): boolean {
21
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment))
22
- }
package/src/operation.ts DELETED
@@ -1,194 +0,0 @@
1
- import { SUPPORTED_METHODS } from './constants.ts'
2
- import { isReference } from './guards.ts'
3
- import { isJsonMimeType } from './mime.ts'
4
- import { resolveRef } from './refs.ts'
5
- import type { Document, MediaTypeObject, OperationObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject } from './types.ts'
6
-
7
- /**
8
- * A single OpenAPI operation: its URL path, HTTP method, and the raw operation object.
9
- *
10
- * `schema` is a live reference into the document, so any in-place `$ref` resolution the resolvers
11
- * perform is visible here too.
12
- */
13
- export type Operation = {
14
- path: string
15
- method: string
16
- schema: OperationObject
17
- }
18
-
19
- /**
20
- * The document plus the operation being read. Shared by the request/response accessors so they can
21
- * resolve `$ref`s against the document.
22
- */
23
- type OperationContext = {
24
- document: Document
25
- operation: Operation
26
- }
27
-
28
- /**
29
- * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
30
- * with no leading or trailing dash.
31
- */
32
- function slugify(value: string): string {
33
- return value
34
- .replace(/[^a-zA-Z0-9]/g, '-')
35
- .replace(/-{2,}/g, '-')
36
- .replace(/^-|-$/g, '')
37
- }
38
-
39
- /**
40
- * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
41
- */
42
- export function getOperationId({ path, method, schema }: Operation): string {
43
- const { operationId } = schema
44
- if (typeof operationId === 'string' && operationId.length > 0) {
45
- return operationId
46
- }
47
- return `${method}_${slugify(path).toLowerCase()}`
48
- }
49
-
50
- /**
51
- * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
52
- */
53
- export function getResponseStatusCodes({ schema }: Operation): Array<string> {
54
- const responses = schema.responses as Record<string, unknown> | undefined
55
- if (!responses || isReference(responses)) {
56
- return []
57
- }
58
- return Object.keys(responses).filter((key) => !key.startsWith('x-') && !!responses[key] && typeof responses[key] === 'object')
59
- }
60
-
61
- /**
62
- * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
63
- */
64
- export function getResponseByStatusCode({ document, operation, statusCode }: OperationContext & { statusCode: string | number }): ResponseObject | false {
65
- const responses = operation.schema.responses as Record<string, ResponseObject | ReferenceObject> | undefined
66
- if (!responses || isReference(responses)) {
67
- return false
68
- }
69
- const response = responses[statusCode]
70
- if (!response) {
71
- return false
72
- }
73
- if (isReference(response)) {
74
- const resolved = resolveRef<ResponseObject>(document, response.$ref)
75
- responses[statusCode] = resolved as ResponseObject
76
- if (!resolved || isReference(resolved)) {
77
- return false
78
- }
79
- return resolved
80
- }
81
- return response
82
- }
83
-
84
- /**
85
- * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
86
- * `undefined` when the operation has no request body.
87
- */
88
- function getRequestBodyContent({ document, operation }: OperationContext): Record<string, MediaTypeObject> | undefined {
89
- const { schema } = operation
90
- let requestBody = schema.requestBody as RequestBodyObject | ReferenceObject | undefined
91
- if (!requestBody) {
92
- return undefined
93
- }
94
- if (isReference(requestBody)) {
95
- const resolved = resolveRef<RequestBodyObject>(document, requestBody.$ref)
96
- ;(schema as { requestBody?: unknown }).requestBody = resolved
97
- if (!resolved || isReference(resolved)) {
98
- return undefined
99
- }
100
- requestBody = resolved
101
- }
102
- return requestBody.content
103
- }
104
-
105
- /**
106
- * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
107
- * Otherwise picks the first JSON-like media type, then the first declared one, as a
108
- * `[mediaType, object]` tuple.
109
- */
110
- export function getRequestContent({
111
- document,
112
- operation,
113
- mediaType,
114
- }: OperationContext & { mediaType?: string }): MediaTypeObject | false | [string, MediaTypeObject] {
115
- const content = getRequestBodyContent({ document, operation })
116
- if (!content) {
117
- return false
118
- }
119
- if (mediaType) {
120
- return mediaType in content ? content[mediaType]! : false
121
- }
122
- const mediaTypes = Object.keys(content)
123
- const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0]
124
- return available ? [available, content[available]!] : false
125
- }
126
-
127
- /**
128
- * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
129
- * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
130
- */
131
- export function getRequestContentType({ document, operation }: OperationContext): string {
132
- const content = getRequestBodyContent({ document, operation })
133
- const mediaTypes = content ? Object.keys(content) : []
134
-
135
- let result = mediaTypes[0] ?? 'application/json'
136
- for (const mt of mediaTypes) {
137
- if (isJsonMimeType(mt)) {
138
- result = mt
139
- }
140
- }
141
- return result
142
- }
143
-
144
- /**
145
- * Builds an `Operation` for every supported HTTP method on every path, in document order.
146
- * `x-` path keys and unresolvable path-item `$ref`s are skipped.
147
- *
148
- * @example
149
- * ```ts
150
- * for (const operation of getOperations(document)) {
151
- * parseOperation(options, operation)
152
- * }
153
- * ```
154
- */
155
- export function getOperations(document: Document): Array<Operation> {
156
- const operations: Array<Operation> = []
157
- const paths = document.paths
158
- if (!paths) {
159
- return operations
160
- }
161
-
162
- for (const path of Object.keys(paths)) {
163
- if (path.startsWith('x-')) {
164
- continue
165
- }
166
-
167
- let pathItem = paths[path] as PathItemObject | ReferenceObject | undefined
168
- if (!pathItem) {
169
- continue
170
- }
171
- if (isReference(pathItem)) {
172
- const resolved = resolveRef<PathItemObject>(document, pathItem.$ref)
173
- ;(paths as Record<string, unknown>)[path] = resolved
174
- if (!resolved || isReference(resolved)) {
175
- continue
176
- }
177
- pathItem = resolved
178
- }
179
-
180
- const item = pathItem as Record<string, unknown>
181
- for (const method of Object.keys(item)) {
182
- if (!SUPPORTED_METHODS.has(method)) {
183
- continue
184
- }
185
- const schema = item[method]
186
- if (!schema || typeof schema !== 'object') {
187
- continue
188
- }
189
- operations.push({ path, method, schema: schema as OperationObject })
190
- }
191
- }
192
-
193
- return operations
194
- }