feat(zod-openapi): support RPC-mode (#124)
* feat(zod-openapi): support RPC-mode * changeset * update readmepull/125/head
parent
27a0c13ae4
commit
e6b20c64b6
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/zod-openapi': minor
|
||||
---
|
||||
|
||||
feat(zod-openapi): support RPC-mode
|
|
@ -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/)
|
||||
|
|
|
@ -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[] = []
|
||||
|
|
|
@ -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>
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue