/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ResponseConfig, RouteConfig, ZodContentObject, ZodRequestBody, } from '@asteasolutions/zod-to-openapi' import { OpenApiGeneratorV3, OpenApiGeneratorV31, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi' import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi' import type { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator' import { zValidator } from '@hono/zod-validator' import { Hono } from 'hono' import type { Context, Env, Handler, Input, MiddlewareHandler, Schema, ToSchema, TypedResponse, } from 'hono' import type { MergePath, MergeSchemaPath } from 'hono/dist/types/types' import type { RemoveBlankRecord } from 'hono/utils/types' import type { AnyZodObject, ZodSchema, ZodError } from 'zod' import { z, ZodType } from 'zod' type RequestTypes = { body?: ZodRequestBody params?: AnyZodObject query?: AnyZodObject cookies?: AnyZodObject headers?: AnyZodObject | ZodType[] } type IsJson = T extends string ? T extends `application/json${infer _Rest}` ? 'json' : never : never type IsForm = T extends string ? T extends | `multipart/form-data${infer _Rest}` | `application/x-www-form-urlencoded${infer _Rest}` ? 'form' : never : never type RequestPart = Part extends keyof R['request'] ? R['request'][Part] : {} type InputTypeBase< R extends RouteConfig, Part extends string, Type extends string > = R['request'] extends RequestTypes ? RequestPart extends AnyZodObject ? { in: { [K in Type]: z.input> } out: { [K in Type]: z.input> } } : {} : {} type InputTypeJson = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsJson extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { in: { json: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } out: { json: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } } : {} : {} : {} : {} type InputTypeForm = R['request'] extends RequestTypes ? R['request']['body'] extends ZodRequestBody ? R['request']['body']['content'] extends ZodContentObject ? IsForm extends never ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { in: { form: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } out: { form: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } } : {} : {} : {} : {} type InputTypeParam = InputTypeBase type InputTypeQuery = InputTypeBase type InputTypeHeader = InputTypeBase type InputTypeCookie = InputTypeBase type OutputType = R['responses'] extends Record ? C extends ResponseConfig ? C['content'] extends ZodContentObject ? IsJson extends never ? {} : C['content'][keyof C['content']]['schema'] extends ZodSchema ? z.infer : {} : {} : {} : {} type Hook = ( result: | { success: true data: T } | { success: false error: ZodError }, c: Context ) => TypedResponse | Promise> | void type ConvertPathType = T extends `${infer _}/{${infer Param}}${infer _}` ? `/:${Param}` : T type HandlerResponse = TypedResponse | Promise> type HonoInit = ConstructorParameters[0]; export class OpenAPIHono< E extends Env = Env, S extends Schema = {}, BasePath extends string = '/' > extends Hono { openAPIRegistry: OpenAPIRegistry constructor(init?: HonoInit) { super(init) this.openAPIRegistry = new OpenAPIRegistry() } openapi = < R extends RouteConfig, I extends Input = InputTypeParam & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType >( route: R, handler: Handler>>, hook?: Hook> ): OpenAPIHono>, BasePath> => { this.openAPIRegistry.registerPath(route) const validators: MiddlewareHandler[] = [] if (route.request?.query) { const validator = zValidator('query', route.request.query as any, hook as any) validators.push(validator as any) } if (route.request?.params) { const validator = zValidator('param', route.request.params as any, hook as any) validators.push(validator as any) } if (route.request?.headers) { const validator = zValidator('header', route.request.headers as any, hook as any) validators.push(validator as any) } if (route.request?.cookies) { const validator = zValidator('cookie', route.request.cookies as any, hook as any) validators.push(validator as any) } const bodyContent = route.request?.body?.content if (bodyContent) { for (const mediaType of Object.keys(bodyContent)) { if (mediaType.startsWith('application/json')) { const schema = bodyContent[mediaType]['schema'] if (schema instanceof ZodType) { const validator = zValidator('json', schema as any, hook as any) validators.push(validator as any) } } if ( mediaType.startsWith('multipart/form-data') || mediaType.startsWith('application/x-www-form-urlencoded') ) { const schema = bodyContent[mediaType]['schema'] if (schema instanceof ZodType) { const validator = zValidator('form', schema as any, hook as any) validators.push(validator as any) } } } } this.on([route.method], route.path.replaceAll(/\/{(.+?)}/g, '/:$1'), ...validators, handler) return this } getOpenAPIDocument = (config: OpenAPIObjectConfig) => { const generator = new OpenApiGeneratorV3(this.openAPIRegistry.definitions) const document = generator.generateDocument(config) return document } getOpenAPI31Document = (config: OpenAPIObjectConfig) => { const generator = new OpenApiGeneratorV31(this.openAPIRegistry.definitions) const document = generator.generateDocument(config) return document } doc = (path: string, config: OpenAPIObjectConfig) => { this.get(path, (c) => { const document = this.getOpenAPIDocument(config) return c.json(document) }) } doc31 = (path: string, config: OpenAPIObjectConfig) => { this.get(path, (c) => { const document = this.getOpenAPI31Document(config) return c.json(document) }) } route< SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string >( path: SubPath, app: Hono ): OpenAPIHono> & S, BasePath> route(path: SubPath): Hono, BasePath> route< SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string >( path: SubPath, app?: Hono ): OpenAPIHono> & S, BasePath> { super.route(path, app as any) if (!(app instanceof OpenAPIHono)) { return this as any } app.openAPIRegistry.definitions.forEach((def) => { switch (def.type) { case 'component': return this.openAPIRegistry.registerComponent( def.componentType, def.name, def.component ) case 'route': return this.openAPIRegistry.registerPath({ ...def.route, path: `${path}${def.route.path}` }) case 'webhook': return this.openAPIRegistry.registerWebhook({ ...def.webhook, path: `${path}${def.webhook.path}` }) case 'schema': return this.openAPIRegistry.register( def.schema._def.openapi._internal.refId, def.schema ) case 'parameter': return this.openAPIRegistry.registerParameter( def.schema._def.openapi._internal.refId, def.schema ) default: { const errorIfNotExhaustive: never = def throw new Error(`Unknown registry type: ${errorIfNotExhaustive}`) } } }) // eslint-disable-next-line @typescript-eslint/no-explicit-any return this as any } } type RoutingPath

= P extends `${infer Head}/{${infer Param}}${infer Tail}` ? `${Head}/:${Param}${RoutingPath}` : P export const createRoute =

& { path: P }>( routeConfig: R ) => { return { ...routeConfig, getRoutingPath(): RoutingPath { return routeConfig.path.replaceAll(/\/{(.+?)}/g, '/:$1') as RoutingPath

} } } extendZodWithOpenApi(z) export { z }