@kubb/plugin-oas
Version:
OpenAPI Specification (OAS) plugin for Kubb, providing core functionality for parsing and processing OpenAPI/Swagger schemas for code generation.
299 lines (265 loc) • 10.6 kB
text/typescript
import type { Plugin, PluginFactoryOptions, PluginManager } from '@kubb/core'
import { BaseGenerator, type FileMetaBase } from '@kubb/core'
import transformers from '@kubb/core/transformers'
import type { KubbFile } from '@kubb/fabric-core/types'
import type { contentType, HttpMethod, Oas, OasTypes, Operation, SchemaObject } from '@kubb/oas'
import type { Fabric } from '@kubb/react-fabric'
import pLimit from 'p-limit'
import type { Generator } from './generators/types.ts'
import type { Exclude, Include, OperationSchemas, Override } from './types.ts'
import { buildOperation, buildOperations } from './utils.tsx'
export type OperationMethodResult<TFileMeta extends FileMetaBase> = Promise<KubbFile.File<TFileMeta> | Array<KubbFile.File<TFileMeta>> | null>
type Context<TOptions, TPluginOptions extends PluginFactoryOptions> = {
fabric: Fabric
oas: Oas
exclude: Array<Exclude> | undefined
include: Array<Include> | undefined
override: Array<Override<TOptions>> | undefined
contentType: contentType | undefined
pluginManager: PluginManager
/**
* Current plugin
*/
plugin: Plugin<TPluginOptions>
mode: KubbFile.Mode
}
export class OperationGenerator<
TPluginOptions extends PluginFactoryOptions = PluginFactoryOptions,
TFileMeta extends FileMetaBase = FileMetaBase,
> extends BaseGenerator<TPluginOptions['resolvedOptions'], Context<TPluginOptions['resolvedOptions'], TPluginOptions>> {
#getOptions(operation: Operation, method: HttpMethod): Partial<TPluginOptions['resolvedOptions']> {
const { override = [] } = this.context
const operationId = operation.getOperationId({ friendlyCase: true })
const contentType = operation.getContentType()
return (
override.find(({ pattern, type }) => {
switch (type) {
case 'tag':
return operation.getTags().some((tag) => tag.name.match(pattern))
case 'operationId':
return !!operationId.match(pattern)
case 'path':
return !!operation.path.match(pattern)
case 'method':
return !!method.match(pattern)
case 'contentType':
return !!contentType.match(pattern)
default:
return false
}
})?.options || {}
)
}
#isExcluded(operation: Operation, method: HttpMethod): boolean {
const { exclude = [] } = this.context
const operationId = operation.getOperationId({ friendlyCase: true })
const contentType = operation.getContentType()
return exclude.some(({ pattern, type }) => {
switch (type) {
case 'tag':
return operation.getTags().some((tag) => tag.name.match(pattern))
case 'operationId':
return !!operationId.match(pattern)
case 'path':
return !!operation.path.match(pattern)
case 'method':
return !!method.match(pattern)
case 'contentType':
return !!contentType.match(pattern)
default:
return false
}
})
}
#isIncluded(operation: Operation, method: HttpMethod): boolean {
const { include = [] } = this.context
const operationId = operation.getOperationId({ friendlyCase: true })
const contentType = operation.getContentType()
return include.some(({ pattern, type }) => {
switch (type) {
case 'tag':
return operation.getTags().some((tag) => tag.name.match(pattern))
case 'operationId':
return !!operationId.match(pattern)
case 'path':
return !!operation.path.match(pattern)
case 'method':
return !!method.match(pattern)
case 'contentType':
return !!contentType.match(pattern)
default:
return false
}
})
}
getSchemas(
operation: Operation,
{
resolveName = (name) => name,
}: {
resolveName?: (name: string) => string
} = {},
): OperationSchemas {
const operationId = operation.getOperationId({ friendlyCase: true })
const method = operation.method
const operationName = transformers.pascalCase(operationId)
const resolveKeys = (schema?: SchemaObject) => (schema?.properties ? Object.keys(schema.properties) : undefined)
const pathParamsSchema = this.context.oas.getParametersSchema(operation, 'path')
const queryParamsSchema = this.context.oas.getParametersSchema(operation, 'query')
const headerParamsSchema = this.context.oas.getParametersSchema(operation, 'header')
const requestSchema = this.context.oas.getRequestSchema(operation)
const statusCodes = operation.getResponseStatusCodes().map((statusCode) => {
const name = statusCode === 'default' ? 'error' : statusCode
const schema = this.context.oas.getResponseSchema(operation, statusCode)
const keys = resolveKeys(schema)
return {
name: resolveName(transformers.pascalCase(`${operationId} ${name}`)),
description: (operation.getResponseByStatusCode(statusCode) as OasTypes.ResponseObject)?.description,
schema,
operation,
operationName,
statusCode: name === 'error' ? undefined : Number(statusCode),
keys,
keysToOmit: keys?.filter((key) => (schema?.properties?.[key] as OasTypes.SchemaObject)?.writeOnly),
}
})
const successful = statusCodes.filter((item) => item.statusCode?.toString().startsWith('2'))
const errors = statusCodes.filter((item) => item.statusCode?.toString().startsWith('4') || item.statusCode?.toString().startsWith('5'))
return {
pathParams: pathParamsSchema
? {
name: resolveName(transformers.pascalCase(`${operationId} PathParams`)),
operation,
operationName,
schema: pathParamsSchema,
keys: resolveKeys(pathParamsSchema),
}
: undefined,
queryParams: queryParamsSchema
? {
name: resolveName(transformers.pascalCase(`${operationId} QueryParams`)),
operation,
operationName,
schema: queryParamsSchema,
keys: resolveKeys(queryParamsSchema) || [],
}
: undefined,
headerParams: headerParamsSchema
? {
name: resolveName(transformers.pascalCase(`${operationId} HeaderParams`)),
operation,
operationName,
schema: headerParamsSchema,
keys: resolveKeys(headerParamsSchema),
}
: undefined,
request: requestSchema
? {
name: resolveName(transformers.pascalCase(`${operationId} ${method === 'get' ? 'queryRequest' : 'mutationRequest'}`)),
description: (operation.schema.requestBody as OasTypes.RequestBodyObject)?.description,
operation,
operationName,
schema: requestSchema,
keys: resolveKeys(requestSchema),
keysToOmit: resolveKeys(requestSchema)?.filter((key) => (requestSchema.properties?.[key] as OasTypes.SchemaObject)?.readOnly),
}
: undefined,
response: {
name: resolveName(transformers.pascalCase(`${operationId} ${method === 'get' ? 'queryResponse' : 'mutationResponse'}`)),
operation,
operationName,
schema: {
oneOf: successful.map((item) => ({ ...item.schema, $ref: item.name })) || undefined,
} as SchemaObject,
},
responses: successful,
errors,
statusCodes,
}
}
async getOperations(): Promise<Array<{ path: string; method: HttpMethod; operation: Operation }>> {
const { oas } = this.context
const paths = oas.getPaths()
return Object.entries(paths).flatMap(([path, methods]) =>
Object.entries(methods)
.map((values) => {
const [method, operation] = values as [HttpMethod, Operation]
if (this.#isExcluded(operation, method)) {
return null
}
if (this.context.include && !this.#isIncluded(operation, method)) {
return null
}
return operation ? { path, method: method as HttpMethod, operation } : null
})
.filter(Boolean),
)
}
async build(...generators: Array<Generator<TPluginOptions>>): Promise<Array<KubbFile.File<TFileMeta>>> {
const operations = await this.getOperations()
const generatorLimit = pLimit(1)
const operationLimit = pLimit(10)
const writeTasks = generators.map((generator) =>
generatorLimit(async () => {
const operationTasks = operations.map(({ operation, method }) =>
operationLimit(async () => {
const options = this.#getOptions(operation, method)
if (generator.type === 'react') {
await buildOperation(operation, {
config: this.context.pluginManager.config,
fabric: this.context.fabric,
Component: generator.Operation,
generator: this,
plugin: {
...this.context.plugin,
options: {
...this.options,
...options,
},
},
})
return []
}
const result = await generator.operation?.({
generator: this,
config: this.context.pluginManager.config,
operation,
plugin: {
...this.context.plugin,
options: {
...this.options,
...options,
},
},
})
return result ?? []
}),
)
const operationResults = await Promise.all(operationTasks)
const opResultsFlat = operationResults.flat()
if (generator.type === 'react') {
await buildOperations(
operations.map((op) => op.operation),
{
fabric: this.context.fabric,
config: this.context.pluginManager.config,
Component: generator.Operations,
generator: this,
plugin: this.context.plugin,
},
)
return []
}
const operationsResult = await generator.operations?.({
generator: this,
config: this.context.pluginManager.config,
operations: operations.map((op) => op.operation),
plugin: this.context.plugin,
})
return [...opResultsFlat, ...(operationsResult ?? [])] as unknown as KubbFile.File<TFileMeta>
}),
)
const nestedResults = await Promise.all(writeTasks)
return nestedResults.flat()
}
}