Skip to content

Commit

Permalink
feat(zod-openapi): supports headers and cookies (#141)
Browse files Browse the repository at this point in the history
* feat(zod-openapi): supports `headers` and `cookies`

* `ZodAny` is not used

* update readme

* changeset
  • Loading branch information
yusukebe authored Aug 24, 2023
1 parent 68ef99c commit f334e99
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-tigers-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/zod-openapi': minor
---

feat: support `headers` and `cookies`
1 change: 0 additions & 1 deletion packages/zod-openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 20 additions & 3 deletions packages/zod-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
124 changes: 124 additions & 0 deletions packages/zod-openapi/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({}),
Expand Down
5 changes: 5 additions & 0 deletions packages/zod-openapi/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"skipLibCheck": false,
"rootDir": "./src",
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

0 comments on commit f334e99

Please sign in to comment.