feat(zod-openapi): supports `headers` and `cookies` (#141)

* feat(zod-openapi): supports `headers` and `cookies`

* `ZodAny` is not used

* update readme

* changeset
pull/142/head
Yusuke Wada 2023-08-25 00:55:16 +09:00 committed by GitHub
parent 68ef99cffc
commit f334e99251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 4 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/zod-openapi': minor
---
feat: support `headers` and `cookies`

View File

@ -6,7 +6,6 @@ _Note: This is not standalone middleware but is hosted on the monorepo "[github.
## Limitations ## 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)`. - An instance of Zod OpenAPI Hono cannot be used as a "subApp" in conjunction with `rootApp.route('/api', subApp)`.
## Usage ## Usage

View File

@ -28,8 +28,8 @@ type RequestTypes = {
body?: ZodRequestBody body?: ZodRequestBody
params?: AnyZodObject params?: AnyZodObject
query?: AnyZodObject query?: AnyZodObject
cookies?: AnyZodObject // not support cookies?: AnyZodObject
headers?: AnyZodObject | ZodType<unknown>[] // not support headers?: AnyZodObject | ZodType<unknown>[]
} }
type IsJson<T> = T extends string 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 InputTypeParam<R extends RouteConfig> = InputTypeBase<R, 'params', 'param'>
type InputTypeQuery<R extends RouteConfig> = InputTypeBase<R, 'query', 'query'> 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> type OutputType<R extends RouteConfig> = R['responses'] extends Record<infer _, infer C>
? C extends ResponseConfig ? C extends ResponseConfig
@ -155,7 +157,12 @@ export class OpenAPIHono<
openapi = < openapi = <
R extends RouteConfig, 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']> P extends string = ConvertPathType<R['path']>
>( >(
route: R, route: R,
@ -176,6 +183,16 @@ export class OpenAPIHono<
validators.push(validator 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 const bodyContent = route.request?.body?.content
if (bodyContent) { if (bodyContent) {

View File

@ -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', () => { describe('JSON', () => {
const RequestSchema = z.object({ const RequestSchema = z.object({
id: z.number().openapi({}), id: z.number().openapi({}),

View File

@ -1,9 +1,14 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"skipLibCheck": false,
"rootDir": "./src", "rootDir": "./src",
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"
], ],
"exclude": [
"node_modules",
"dist"
]
} }