feat(zod-openapi): infer env from routeMiddleware (#807)

* feat(zod-openapi): infer env from routeMiddleware

* chore(zod-openapi): add changeset for routeMiddleware Env inference

Close #715
pull/811/head
oberbeck 2024-11-07 04:11:30 +01:00 committed by GitHub
parent c6bbcb8f9d
commit 2eec6f6fd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 165 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/zod-openapi': minor
---
introduce routeMiddleware Env inference

View File

@ -207,6 +207,68 @@ export type OpenAPIHonoOptions<E extends Env> = {
} }
type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & OpenAPIHonoOptions<E> type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & OpenAPIHonoOptions<E>
/**
* Turns `T | T[] | undefined` into `T[]`
*/
type AsArray<T> = T extends undefined // TODO move to utils?
? []
: T extends any[]
? T
: [T]
/**
* Like simplify but recursive
*/
export type DeepSimplify<T> = {
// TODO move to utils?
[KeyType in keyof T]: T[KeyType] extends object ? DeepSimplify<T[KeyType]> : T[KeyType]
} & {}
/**
* Helper to infer generics from {@link MiddlewareHandler}
*/
export type OfHandlerType<T extends MiddlewareHandler> = 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 MiddlewareHandler<any, any, any>[]> = M extends [
infer First,
infer Second,
...infer Rest
]
? First extends MiddlewareHandler<any, any, any>
? Second extends MiddlewareHandler<any, any, any>
? Rest extends MiddlewareHandler<any, any, any>[] // Ensure Rest is an array of MiddlewareHandler
? MiddlewareToHandlerType<
[
MiddlewareHandler<
DeepSimplify<OfHandlerType<First>['env'] & OfHandlerType<Second>['env']>, // Combine envs
OfHandlerType<First>['path'], // Keep path from First
OfHandlerType<First>['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< export type RouteHandler<
R extends RouteConfig, R extends RouteConfig,
E extends Env = Env, E extends Env = Env,
@ -317,7 +379,10 @@ export class OpenAPIHono<
>( >(
{ middleware: routeMiddleware, ...route }: R, { middleware: routeMiddleware, ...route }: R,
handler: Handler< handler: Handler<
E, // use the env from the middleware if it's defined
R['middleware'] extends MiddlewareHandler[] | MiddlewareHandler
? OfHandlerType<MiddlewareToHandlerType<AsArray<R['middleware']>>>['env'] & E
: E,
P, P,
I, I,
// If response type is defined, only TypedResponse is allowed. // If response type is defined, only TypedResponse is allowed.

View File

@ -1,6 +1,7 @@
import type { Env, Hono, ToSchema } from 'hono' import type { Env, Hono, ToSchema } from 'hono'
import { assertType, describe, expectTypeOf, it } from 'vitest' 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 { ExtractSchema } from 'hono/types'
import type { Equal, Expect } from 'hono/utils/types' import type { Equal, Expect } from 'hono/utils/types'
@ -234,3 +235,95 @@ describe('coerce', () => {
type verify = Expect<Equal<Expected, Actual>> type verify = Expect<Equal<Expected, Actual>>
}) })
}) })
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<Example>['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<Equal<typeof c.var.foo, string>>
type verifyBar = Expect<Equal<typeof c.var.bar, number>>
type verifyToo = Expect<Equal<typeof c.var.too, Symbol>>
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<Equal<typeof c.var.too, Symbol>>
return c.json({})
}
)
})
})