feat(zod-openapi): support RPC-mode (#124)

* feat(zod-openapi): support RPC-mode

* changeset

* update readme
pull/125/head
Yusuke Wada 2023-08-19 10:40:55 +09:00 committed by GitHub
parent 27a0c13ae4
commit e6b20c64b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/zod-openapi': minor
---
feat(zod-openapi): support RPC-mode

View File

@ -183,6 +183,24 @@ import { prettyJSON } from 'hono/pretty-json'
app.use('/doc/*', prettyJSON())
```
### RPC-mode
Zod OpenAPI Hono supports Hono's RPC-mode. You can create the types for passing Hono Client:
```ts
import { hc } from 'hono/client'
const appRoutes = app.openapi(route, (c) => {
const data = c.req.valid('json')
return c.jsonT({
id: data.id,
message: 'Success',
})
})
const client = hc<typeof appRoutes>('http://localhost:8787/')
```
## References
- [Hono](https://hono.dev/)

View File

@ -11,7 +11,7 @@ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'
import type { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import type { Context, Input, TypedResponse } from 'hono'
import type { Context, Input, Schema, TypedResponse } from 'hono'
import type { Env, Handler, MiddlewareHandler } from 'hono'
import type { AnyZodObject, ZodSchema, ZodError } from 'zod'
import { z, ZodType } from 'zod'
@ -40,7 +40,7 @@ type IsForm<T> = T extends string
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request']
? R['request'][Part]
: never
: {}
type InputTypeBase<
R extends RouteConfig,
@ -49,6 +49,7 @@ type InputTypeBase<
> = R['request'] extends RequestTypes
? RequestPart<R, Part> extends AnyZodObject
? {
in: { [K in Type]: z.input<RequestPart<R, Part>> }
out: { [K in Type]: z.input<RequestPart<R, Part>> }
}
: {}
@ -61,6 +62,11 @@ 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.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
json: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
@ -79,6 +85,11 @@ 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.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
>
}
out: {
form: z.input<
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
@ -137,7 +148,7 @@ export class OpenAPIHono<E extends Env = Env, S = {}, BasePath extends string =
route: R,
handler: Handler<E, R['path'], I, OutputType<R>>,
hook?: Hook<I, E, R['path'], OutputType<R>>
) => {
): Hono<E, Schema<R['method'], R['path'], I['in'], OutputType<R>>, BasePath> => {
this.#registry.registerPath(route)
const validators: MiddlewareHandler[] = []

View File

@ -1,5 +1,9 @@
// eslint-disable-next-line node/no-extraneous-import
import { describe, it, expect } from 'vitest'
/* eslint-disable node/no-extraneous-import */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Env } from 'hono'
import type { Hono } from 'hono'
import { describe, it, expect, expectTypeOf } from 'vitest'
import type { Schema } from 'zod'
import { OpenAPIHono, createRoute, z } from '../src'
describe('Basic - params', () => {
@ -338,7 +342,7 @@ describe('Form', () => {
schema: PostsSchema,
},
},
description: 'Get the posts',
description: 'Post the post',
},
},
})
@ -382,3 +386,75 @@ describe('Form', () => {
expect(res.status).toBe(400)
})
})
describe('Types', () => {
const RequestSchema = z.object({
id: z.number().openapi({}),
title: z.string().openapi({}),
})
const PostsSchema = 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: PostsSchema,
},
},
description: 'Post the post',
},
},
})
const app = new OpenAPIHono()
const appRoutes = app.openapi(route, (c) => {
const data = c.req.valid('json')
return c.jsonT({
id: data.id,
message: 'Success',
})
})
it('Should return correct types', () => {
type H = Hono<
Env,
Schema<{
'/posts': {
$post: {
input: {
json: {
title: string
id: number
}
}
output: {
id: number
message: string
}
}
}
}>,
'/'
>
expectTypeOf(appRoutes).toMatchTypeOf<H>
})
})