From f334e99251cdabc8be9334eec7eb7d9a450d8e35 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Fri, 25 Aug 2023 00:55:16 +0900 Subject: [PATCH] feat(zod-openapi): supports `headers` and `cookies` (#141) * feat(zod-openapi): supports `headers` and `cookies` * `ZodAny` is not used * update readme * changeset --- .changeset/dirty-tigers-roll.md | 5 + packages/zod-openapi/README.md | 1 - packages/zod-openapi/src/index.ts | 23 ++++- packages/zod-openapi/test/index.test.ts | 124 ++++++++++++++++++++++++ packages/zod-openapi/tsconfig.json | 5 + 5 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 .changeset/dirty-tigers-roll.md diff --git a/.changeset/dirty-tigers-roll.md b/.changeset/dirty-tigers-roll.md new file mode 100644 index 00000000..ff5192f8 --- /dev/null +++ b/.changeset/dirty-tigers-roll.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': minor +--- + +feat: support `headers` and `cookies` diff --git a/packages/zod-openapi/README.md b/packages/zod-openapi/README.md index ac42ca7f..dfe94139 100644 --- a/packages/zod-openapi/README.md +++ b/packages/zod-openapi/README.md @@ -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 diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index bfc1de50..4cb8fdcc 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -28,8 +28,8 @@ type RequestTypes = { body?: ZodRequestBody params?: AnyZodObject query?: AnyZodObject - cookies?: AnyZodObject // not support - headers?: AnyZodObject | ZodType[] // not support + cookies?: AnyZodObject + headers?: AnyZodObject | ZodType[] } type IsJson = T extends string @@ -111,6 +111,8 @@ type InputTypeForm = R['request'] extends RequestTypes type InputTypeParam = InputTypeBase type InputTypeQuery = InputTypeBase +type InputTypeHeader = InputTypeBase +type InputTypeCookie = InputTypeBase type OutputType = R['responses'] extends Record ? C extends ResponseConfig @@ -155,7 +157,12 @@ export class OpenAPIHono< openapi = < R extends RouteConfig, - I extends Input = InputTypeParam & InputTypeQuery & InputTypeForm & InputTypeJson, + I extends Input = InputTypeParam & + InputTypeQuery & + InputTypeHeader & + InputTypeCookie & + InputTypeForm & + InputTypeJson, P extends string = ConvertPathType >( 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) { diff --git a/packages/zod-openapi/test/index.test.ts b/packages/zod-openapi/test/index.test.ts index 91cecd80..19b20b4e 100644 --- a/packages/zod-openapi/test/index.test.ts +++ b/packages/zod-openapi/test/index.test.ts @@ -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({}), diff --git a/packages/zod-openapi/tsconfig.json b/packages/zod-openapi/tsconfig.json index 6c1a3990..8eaee1b9 100644 --- a/packages/zod-openapi/tsconfig.json +++ b/packages/zod-openapi/tsconfig.json @@ -1,9 +1,14 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "skipLibCheck": false, "rootDir": "./src", }, "include": [ "src/**/*.ts" ], + "exclude": [ + "node_modules", + "dist" + ] } \ No newline at end of file