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
|
## 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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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({}),
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue