diff --git a/.changeset/shy-pigs-buy.md b/.changeset/shy-pigs-buy.md new file mode 100644 index 00000000..56ea9ac6 --- /dev/null +++ b/.changeset/shy-pigs-buy.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': minor +--- + +introduce routeMiddleware Env inference diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index 4287e8e7..2af11c43 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -207,6 +207,68 @@ export type OpenAPIHonoOptions = { } type HonoInit = ConstructorParameters[0] & OpenAPIHonoOptions +/** + * Turns `T | T[] | undefined` into `T[]` + */ +type AsArray = T extends undefined // TODO move to utils? + ? [] + : T extends any[] + ? T + : [T] + +/** + * Like simplify but recursive + */ +export type DeepSimplify = { + // TODO move to utils? + [KeyType in keyof T]: T[KeyType] extends object ? DeepSimplify : T[KeyType] +} & {} + +/** + * Helper to infer generics from {@link MiddlewareHandler} + */ +export type OfHandlerType = T extends MiddlewareHandler< + infer E, + infer P, + infer I +> + ? { + env: E + path: P + input: I + } + : never + +/** + * Reduce a tuple of middleware handlers into a single + * handler representing the composition of all + * handlers. + */ +export type MiddlewareToHandlerType[]> = M extends [ + infer First, + infer Second, + ...infer Rest +] + ? First extends MiddlewareHandler + ? Second extends MiddlewareHandler + ? Rest extends MiddlewareHandler[] // Ensure Rest is an array of MiddlewareHandler + ? MiddlewareToHandlerType< + [ + MiddlewareHandler< + DeepSimplify['env'] & OfHandlerType['env']>, // Combine envs + OfHandlerType['path'], // Keep path from First + OfHandlerType['input'] // Keep input from First + >, + ...Rest + ] + > + : never + : never + : never + : M extends [infer Last] + ? Last // Return the last remaining handler in the array + : never + export type RouteHandler< R extends RouteConfig, E extends Env = Env, @@ -317,7 +379,10 @@ export class OpenAPIHono< >( { middleware: routeMiddleware, ...route }: R, handler: Handler< - E, + // use the env from the middleware if it's defined + R['middleware'] extends MiddlewareHandler[] | MiddlewareHandler + ? OfHandlerType>>['env'] & E + : E, P, I, // If response type is defined, only TypedResponse is allowed. diff --git a/packages/zod-openapi/test/index.test-d.ts b/packages/zod-openapi/test/index.test-d.ts index b9f2df26..b09010a0 100644 --- a/packages/zod-openapi/test/index.test-d.ts +++ b/packages/zod-openapi/test/index.test-d.ts @@ -1,6 +1,7 @@ import type { Env, Hono, ToSchema } from 'hono' import { assertType, describe, expectTypeOf, it } from 'vitest' -import { OpenAPIHono, createRoute, z } from '../src/index' +import { MiddlewareToHandlerType, OfHandlerType, OpenAPIHono, createRoute, z } from '../src/index' +import { createMiddleware } from 'hono/factory' import type { ExtractSchema } from 'hono/types' import type { Equal, Expect } from 'hono/utils/types' @@ -234,3 +235,95 @@ describe('coerce', () => { type verify = Expect> }) }) + +describe('Middleware', () => { + it('Should merge Env', async () => { + const middlewareA = createMiddleware<{ + Variables: { foo: string } + }>(async (c, next) => { + c.set('foo', 'abc') + next() + }) + + const middlewareB = createMiddleware<{ + Variables: { bar: number } + }>(async (c, next) => { + c.set('bar', 321) + next() + }) + + type Example = MiddlewareToHandlerType<[typeof middlewareA, typeof middlewareB]> + + type verify = Expect< + Equal< + OfHandlerType['env'], + { + Variables: { foo: string; bar: number } + } + > + > + }) + + it('Should infer Env from router middleware', async () => { + const app = new OpenAPIHono<{Variables: { too: Symbol }}>() + app.openapi( + createRoute({ + method: 'get', + path: '/books', + middleware: [ + createMiddleware<{ + Variables: { foo: string } + }>((c, next) => { + c.set('foo', 'abc') + return next() + }), + createMiddleware<{ + Variables: { bar: number } + }>((c, next) => { + c.set('bar', 321) + return next() + }), + ] as const, + responses: { + 200: { + description: 'response', + }, + }, + }), + (c) => { + c.var.foo + c.var.bar + c.var.too + + type verifyFoo = Expect> + type verifyBar = Expect> + type verifyToo = Expect> + + return c.json({}) + } + ) + }) + + it('Should infer Env root when no middleware provided', async () => { + const app = new OpenAPIHono<{Variables: { too: Symbol }}>() + app.openapi( + createRoute({ + method: 'get', + path: '/books', + middleware: undefined, + responses: { + 200: { + description: 'response', + }, + }, + }), + (c) => { + c.var.too + + type verify = Expect> + + return c.json({}) + } + ) + }) +})