/* 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 { StatusCode } from 'hono/utils/http-status' import type { Prettify, RemoveBlankRecord } from 'hono/utils/types' import { mergePath } from 'hono/utils/url' import type { AnyZodObject, ZodSchema, ZodError } from 'zod' import { z, ZodType } from 'zod' type MaybePromise = Promise | T export 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 ExtractContent = T extends { [K in keyof T]: infer A } ? A extends Record<'schema', ZodSchema> ? z.infer : never : never export type RouteConfigToTypedResponse = { [Status in keyof R['responses'] & StatusCode]: IsJson< keyof R['responses'][Status]['content'] > extends never ? TypedResponse<{}, Status, string> : TypedResponse, Status, 'json'> }[keyof R['responses'] & StatusCode] export type Hook = ( result: | { success: true data: T } | { success: false error: ZodError }, c: Context ) => R type ConvertPathType = T extends `${infer Start}/{${infer Param}}${infer Rest}` ? `${Start}/:${Param}${ConvertPathType}` : T 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: number]: { content: { [mediaType: string]: ZodMediaTypeObject } } } } ? MaybePromise> : MaybePromise> | MaybePromise > export type RouteHook< R extends RouteConfig, E extends Env = Env, I extends Input = InputTypeParam & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType > = Hook< I, E, P, RouteConfigToTypedResponse | Response | Promise | void | Promise > 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 } /** * * @param {RouteConfig} route - The route definition which you create with `createRoute()`. * @param {Handler} handler - The handler. If you want to return a JSON object, you should specify the status code with `c.json()`. * @param {Hook} hook - Optional. The hook method defines what it should do after validation. * @example * app.openapi( * route, * (c) => { * // ... * return c.json( * { * age: 20, * name: 'Young man', * }, * 200 // You should specify the status code even if it's 200. * ) * }, * (result, c) => { * if (!result.success) { * return c.json( * { * code: 400, * message: 'Custom Message', * }, * 400 * ) * } * } *) */ 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: number]: { content: { [mediaType: string]: ZodMediaTypeObject } } } } ? MaybePromise> : MaybePromise> | MaybePromise >, hook: | Hook< I, E, P, RouteConfigToTypedResponse | Response | Promise | void | Promise > | undefined = this.defaultHook ): OpenAPIHono< E, S & ToSchema, I, RouteConfigToTypedResponse>, 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: any) { 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: any) { 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 }