From 85b8e1035f4fd8661e295c26ed246cc229044d3f Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Mon, 4 Dec 2023 17:38:45 +0800 Subject: [PATCH] feat(zod-openapi): use z.input to infer types for inputs of the input --- packages/zod-openapi/src/index.ts | 6 +- packages/zod-openapi/test/index.test-d.ts | 166 ++++++++++++++++++++++ packages/zod-openapi/test/index.test.ts | 105 +++++++++----- 3 files changed, 237 insertions(+), 40 deletions(-) create mode 100644 packages/zod-openapi/test/index.test-d.ts diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index 204db79e..d2e8a125 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -64,7 +64,7 @@ type InputTypeBase< > = R['request'] extends RequestTypes ? RequestPart extends AnyZodObject ? { - in: { [K in Type]: z.infer> } + in: { [K in Type]: z.input> } out: { [K in Type]: z.output> } } : {} @@ -78,7 +78,7 @@ type InputTypeJson = R['request'] extends RequestTypes : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { in: { - json: z.infer< + json: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } @@ -101,7 +101,7 @@ type InputTypeForm = R['request'] extends RequestTypes : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { in: { - form: z.infer< + form: z.input< R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] > } diff --git a/packages/zod-openapi/test/index.test-d.ts b/packages/zod-openapi/test/index.test-d.ts new file mode 100644 index 00000000..d8cd0071 --- /dev/null +++ b/packages/zod-openapi/test/index.test-d.ts @@ -0,0 +1,166 @@ +import type { Hono, Env, ToSchema } from 'hono' +import { describe, it, expectTypeOf, assertType } from 'vitest' +import { OpenAPIHono, createRoute, z } from '../src/index' + +describe('Types', () => { + const RequestSchema = z.object({ + id: z.number().openapi({}), + title: z.string().openapi({}), + }) + + const PostSchema = z + .object({ + id: z.number().openapi({}), + message: z.string().openapi({}), + }) + .openapi('Post') + + const route = createRoute({ + method: 'post', + path: '/posts', + request: { + body: { + content: { + 'application/json': { + schema: RequestSchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: PostSchema, + }, + }, + description: 'Post a post', + }, + }, + }) + + const app = new OpenAPIHono() + + const appRoutes = app.openapi(route, (c) => { + const data = c.req.valid('json') + assertType(data.id) + return c.jsonT({ + id: data.id, + message: 'Success', + }) + }) + + it('Should return correct types', () => { + type H = Hono< + Env, + ToSchema< + 'post', + '/posts', + { + json: { + title: string + id: number + } + }, + { + id: number + message: string + } + >, + '/' + > + expectTypeOf(appRoutes).toMatchTypeOf() + }) +}) + +describe('Input types', () => { + const ParamsSchema = z.object({ + id: z.string().transform(Number).openapi({ + param: { + name: 'id', + in: 'path', + }, + example: 123, + }), + }) + + const QuerySchema = z.object({ + age: z.string().transform(Number).openapi({ + param: { + name: 'age', + in: 'query', + }, + example: 42 + }), + }) + + const BodySchema = z.object({ + sex: z.enum(['male', 'female']).openapi({}) + }).openapi('User') + + const UserSchema = z + .object({ + id: z.number().openapi({ + example: 123, + }), + name: z.string().openapi({ + example: 'John Doe', + }), + age: z.number().openapi({ + example: 42, + }), + sex: z.enum(['male', 'female']).openapi({ + example: 'male', + }) + }) + .openapi('User') + + const route = createRoute({ + method: 'patch', + path: '/users/{id}', + request: { + params: ParamsSchema, + query: QuerySchema, + body: { + content: { + 'application/json': { + schema: BodySchema, + }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: UserSchema, + }, + }, + description: 'Update a user', + }, + }, + }) + + + it('Should return correct types', () => { + const app = new OpenAPIHono() + + app.openapi(route, (c) => { + const { id } = c.req.valid('param') + assertType(id) + + const { age } = c.req.valid('query') + assertType(age) + + const { sex } = c.req.valid('json') + assertType<'male' | 'female'>(sex) + + return c.jsonT({ + id, + age, + sex, + name: 'Ultra-man', + }) + }) + }) +}) diff --git a/packages/zod-openapi/test/index.test.ts b/packages/zod-openapi/test/index.test.ts index a888bf20..2d0e494b 100644 --- a/packages/zod-openapi/test/index.test.ts +++ b/packages/zod-openapi/test/index.test.ts @@ -550,27 +550,58 @@ describe('Form', () => { }) }) -describe('Types', () => { - const RequestSchema = z.object({ - id: z.number().openapi({}), - title: z.string().openapi({}), +describe('Input types', () => { + const ParamsSchema = z.object({ + id: z.string().transform(Number).openapi({ + param: { + name: 'id', + in: 'path', + }, + example: 123, + }), }) - const PostSchema = z + const QuerySchema = z.object({ + age: z.string().transform(Number).openapi({ + param: { + name: 'age', + in: 'query', + }, + example: 42 + }), + }) + + const BodySchema = z.object({ + sex: z.enum(['male', 'female']).openapi({}) + }).openapi('User') + + const UserSchema = z .object({ - id: z.number().openapi({}), - message: z.string().openapi({}), + id: z.number().openapi({ + example: 123, + }), + name: z.string().openapi({ + example: 'John Doe', + }), + age: z.number().openapi({ + example: 42, + }), + sex: z.enum(['male', 'female']).openapi({ + example: 'male', + }) }) - .openapi('Post') + .openapi('User') const route = createRoute({ - method: 'post', - path: '/posts', + method: 'patch', + path: '/users/{id}', request: { + params: ParamsSchema, + query: QuerySchema, body: { content: { 'application/json': { - schema: RequestSchema, + schema: BodySchema, }, }, }, @@ -579,44 +610,44 @@ describe('Types', () => { 200: { content: { 'application/json': { - schema: PostSchema, + schema: UserSchema, }, }, - description: 'Post a post', + description: 'Update a user', }, }, }) const app = new OpenAPIHono() - const appRoutes = app.openapi(route, (c) => { - const data = c.req.valid('json') + app.openapi(route, (c) => { + const { id } = c.req.valid('param') + const { age } = c.req.valid('query') + const { sex } = c.req.valid('json') + return c.jsonT({ - id: data.id, - message: 'Success', + id, + age, + sex, + name: 'Ultra-man', }) }) - it('Should return correct types', () => { - type H = Hono< - Env, - ToSchema< - 'post', - '/posts', - { - json: { - title: string - id: number - } - }, - { - id: number - message: string - } - >, - '/' - > - expectTypeOf(appRoutes).toMatchTypeOf + it('Should return 200 response with correct typed contents', async () => { + const res = await app.request('/users/123?age=42', { + method: 'PATCH', + body: JSON.stringify({ sex: 'male' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ + id: 123, + age: 42, + sex: 'male', + name: 'Ultra-man' + }) }) })