Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
]
}