/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { ResponseConfig, RouteConfig as RouteConfigBase, ZodContentObject, ZodMediaTypeObject, 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/types' import type { RemoveBlankRecord } from 'hono/utils/types' import { mergePath } from 'hono/utils/url' import type { AnyZodObject, ZodSchema, ZodError } from 'zod' import { z, ZodType } from 'zod' type RouteConfig = RouteConfigBase & { middleware?: MiddlewareHandler | MiddlewareHandler[] } 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.output> } } : {} : {} 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']] extends Record< 'schema', ZodSchema > ? { in: { json: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } out: { json: z.output< 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']] extends Record< 'schema', ZodSchema > ? { in: { form: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } out: { form: z.output< 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']] extends Record<'schema', ZodSchema> ? z.infer : {} : {} : {} : {} export type Hook = ( result: | { success: true data: T } | { success: false error: ZodError }, c: Context ) => TypedResponse | Promise> | Response | Promise | void type ConvertPathType = T extends `${infer Start}/{${infer Param}}${infer Rest}` ? `${Start}/:${Param}${ConvertPathType}` : T type HandlerTypedResponse = TypedResponse | Promise> type HandlerAllResponse = | Response | Promise | TypedResponse | Promise> export type OpenAPIHonoOptions = { defaultHook?: Hook } type HonoInit = ConstructorParameters[0] & OpenAPIHonoOptions export type RouteHandler< R extends RouteConfig, E extends Env = Env, I extends Input = InputTypeParam & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType > = Handler< E, P, I, // If response type is defined, only TypedResponse is allowed. R extends { responses: { [statusCode: string]: { content: { [mediaType: string]: ZodMediaTypeObject } } } } ? HandlerTypedResponse> : HandlerAllResponse> > export type RouteHook< R extends RouteConfig, E extends Env = Env, I extends Input = InputTypeParam & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType > = Hook> export type OpenAPIObjectConfigure = | OpenAPIObjectConfig | ((context: Context) => OpenAPIObjectConfig) export class OpenAPIHono< E extends Env = Env, S extends Schema = {}, BasePath extends string = '/' > extends Hono { openAPIRegistry: OpenAPIRegistry defaultHook?: OpenAPIHonoOptions['defaultHook'] constructor(init?: HonoInit) { super(init) this.openAPIRegistry = new OpenAPIRegistry() this.defaultHook = init?.defaultHook } openapi = < R extends RouteConfig, I extends Input = InputTypeParam & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType >( route: R, handler: Handler< E, P, I, // If response type is defined, only TypedResponse is allowed. R extends { responses: { [statusCode: string]: { content: { [mediaType: string]: ZodMediaTypeObject } } } } ? HandlerTypedResponse> : HandlerAllResponse> >, hook: Hook> | undefined = this.defaultHook ): OpenAPIHono< E, S & ToSchema, I['in'], OutputType>, 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 (!bodyContent[mediaType]) { continue } const schema = (bodyContent[mediaType] as ZodMediaTypeObject)['schema'] if (!(schema instanceof ZodType)) { continue } if (mediaType.startsWith('application/json')) { const validator = zValidator('json', schema, hook as any) validators.push(validator as any) } if ( mediaType.startsWith('multipart/form-data') || mediaType.startsWith('application/x-www-form-urlencoded') ) { const validator = zValidator('form', schema, hook as any) validators.push(validator as any) } } } const middleware = route.middleware ? (Array.isArray(route.middleware) ? route.middleware : [route.middleware]) : [] this.on([route.method], route.path.replaceAll(/\/{(.+?)}/g, '/:$1'), ...middleware, ...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: P, configure: OpenAPIObjectConfigure ): OpenAPIHono, BasePath> => { return this.get(path, (c) => { const config = typeof configure === 'function' ? configure(c) : configure try { const document = this.getOpenAPIDocument(config) return c.json(document) } catch (e) { return c.json(e, 500) } }) as any } doc31 =

( path: P, configure: OpenAPIObjectConfigure ): OpenAPIHono, BasePath> => { return this.get(path, (c) => { const config = typeof configure === 'function' ? configure(c) : configure try { const document = this.getOpenAPI31Document(config) return c.json(document) } catch (e) { return c.json(e, 500) } }) as any } 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> { const pathForOpenAPI = path.replaceAll(/:([^\/]+)/g, '{$1}') 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: mergePath(pathForOpenAPI, def.route.path), }) case 'webhook': return this.openAPIRegistry.registerWebhook({ ...def.webhook, path: mergePath(pathForOpenAPI, 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 } basePath(path: SubPath): OpenAPIHono> { return new OpenAPIHono({ ...(super.basePath(path) as any), defaultHook: this.defaultHook }) } } type RoutingPath

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

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

}, } return Object.defineProperty(route, 'getRoutingPath', { enumerable: false }) } extendZodWithOpenApi(z) export { z }