feat(zod-openapi): supports `headers` and `cookies` (#141)
* feat(zod-openapi): supports `headers` and `cookies` * `ZodAny` is not used * update readme * changesetpull/142/head
parent
68ef99cffc
commit
f334e99251
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': minor
|
||||
---
|
||||
|
||||
feat: support `headers` and `cookies`
|
|
@ -6,7 +6,6 @@ _Note: This is not standalone middleware but is hosted on the monorepo "[github.
|
|||
|
||||
## Limitations
|
||||
|
||||
- Currently, it does not support validation of _headers_ and _cookies_.
|
||||
- An instance of Zod OpenAPI Hono cannot be used as a "subApp" in conjunction with `rootApp.route('/api', subApp)`.
|
||||
|
||||
## Usage
|
||||
|
|
|
@ -28,8 +28,8 @@ type RequestTypes = {
|
|||
body?: ZodRequestBody
|
||||
params?: AnyZodObject
|
||||
query?: AnyZodObject
|
||||
cookies?: AnyZodObject // not support
|
||||
headers?: AnyZodObject | ZodType<unknown>[] // not support
|
||||
cookies?: AnyZodObject
|
||||
headers?: AnyZodObject | ZodType<unknown>[]
|
||||
}
|
||||
|
||||
type IsJson<T> = T extends string
|
||||
|
@ -111,6 +111,8 @@ type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
|
|||
|
||||
type InputTypeParam<R extends RouteConfig> = InputTypeBase<R, 'params', 'param'>
|
||||
type InputTypeQuery<R extends RouteConfig> = InputTypeBase<R, 'query', 'query'>
|
||||
type InputTypeHeader<R extends RouteConfig> = InputTypeBase<R, 'headers', 'header'>
|
||||
type InputTypeCookie<R extends RouteConfig> = InputTypeBase<R, 'cookies', 'cookie'>
|
||||
|
||||
type OutputType<R extends RouteConfig> = R['responses'] extends Record<infer _, infer C>
|
||||
? C extends ResponseConfig
|
||||
|
@ -155,7 +157,12 @@ export class OpenAPIHono<
|
|||
|
||||
openapi = <
|
||||
R extends RouteConfig,
|
||||
I extends Input = InputTypeParam<R> & InputTypeQuery<R> & InputTypeForm<R> & InputTypeJson<R>,
|
||||
I extends Input = InputTypeParam<R> &
|
||||
InputTypeQuery<R> &
|
||||
InputTypeHeader<R> &
|
||||
InputTypeCookie<R> &
|
||||
InputTypeForm<R> &
|
||||
InputTypeJson<R>,
|
||||
P extends string = ConvertPathType<R['path']>
|
||||
>(
|
||||
route: R,
|
||||
|
@ -176,6 +183,16 @@ export class OpenAPIHono<
|
|||
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) {
|
||||
|
|
|
@ -225,6 +225,130 @@ describe('Query', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Header', () => {
|
||||
const HeaderSchema = z.object({
|
||||
'x-request-id': z.string().uuid(),
|
||||
})
|
||||
|
||||
const PingSchema = z
|
||||
.object({
|
||||
'x-request-id': z.string().uuid(),
|
||||
})
|
||||
.openapi('Post')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/ping',
|
||||
request: {
|
||||
headers: HeaderSchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PingSchema,
|
||||
},
|
||||
},
|
||||
description: 'Ping',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const headerData = c.req.valid('header')
|
||||
const xRequestId = headerData['x-request-id']
|
||||
return c.jsonT({
|
||||
'x-request-id': xRequestId,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const res = await app.request('/ping', {
|
||||
headers: {
|
||||
'x-request-id': '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
'x-request-id': '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const res = await app.request('/ping', {
|
||||
headers: {
|
||||
'x-request-id': 'invalid-strings',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cookie', () => {
|
||||
const CookieSchema = z.object({
|
||||
debug: z.enum(['0', '1']),
|
||||
})
|
||||
|
||||
const UserSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
debug: z.enum(['0', '1']),
|
||||
})
|
||||
.openapi('User')
|
||||
|
||||
const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/api/user',
|
||||
request: {
|
||||
cookies: CookieSchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UserSchema,
|
||||
},
|
||||
},
|
||||
description: 'Get a user',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const app = new OpenAPIHono()
|
||||
|
||||
app.openapi(route, (c) => {
|
||||
const { debug } = c.req.valid('cookie')
|
||||
return c.jsonT({
|
||||
name: 'foo',
|
||||
debug,
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct contents', async () => {
|
||||
const res = await app.request('/api/user', {
|
||||
headers: {
|
||||
Cookie: 'debug=1',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
name: 'foo',
|
||||
debug: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response with correct contents', async () => {
|
||||
const res = await app.request('/api/user', {
|
||||
headers: {
|
||||
Cookie: 'debug=2',
|
||||
},
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', () => {
|
||||
const RequestSchema = z.object({
|
||||
id: z.number().openapi({}),
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": false,
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue