Skip to content

Commit

Permalink
feat(zod-openapi): use z.input to infer types for inputs of the input
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen committed Dec 4, 2023
1 parent 2f7b583 commit 85b8e10
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 40 deletions.
6 changes: 3 additions & 3 deletions packages/zod-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type InputTypeBase<
> = R['request'] extends RequestTypes
? RequestPart<R, Part> extends AnyZodObject
? {
in: { [K in Type]: z.infer<RequestPart<R, Part>> }
in: { [K in Type]: z.input<RequestPart<R, Part>> }
out: { [K in Type]: z.output<RequestPart<R, Part>> }
}
: {}
Expand All @@ -78,7 +78,7 @@ type InputTypeJson<R extends RouteConfig> = R['request'] extends RequestTypes
: R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any>
? {
in: {
json: z.infer<
json: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
Expand All @@ -101,7 +101,7 @@ type InputTypeForm<R extends RouteConfig> = R['request'] extends RequestTypes
: R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema<any>
? {
in: {
form: z.infer<
form: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
Expand Down
166 changes: 166 additions & 0 deletions packages/zod-openapi/test/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<number>(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<H>()
})
})

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<number>(id)

const { age } = c.req.valid('query')
assertType<number>(age)

const { sex } = c.req.valid('json')
assertType<'male' | 'female'>(sex)

return c.jsonT({
id,
age,
sex,
name: 'Ultra-man',
})
})
})
})
105 changes: 68 additions & 37 deletions packages/zod-openapi/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand All @@ -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<H>
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'
})
})
})

Expand Down

0 comments on commit 85b8e10

Please sign in to comment.