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())
|
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
|
## References
|
||||||
|
|
||||||
- [Hono](https://hono.dev/)
|
- [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 type { OpenAPIObjectConfig } from '@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator'
|
||||||
import { zValidator } from '@hono/zod-validator'
|
import { zValidator } from '@hono/zod-validator'
|
||||||
import { Hono } from 'hono'
|
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 { Env, Handler, MiddlewareHandler } from 'hono'
|
||||||
import type { AnyZodObject, ZodSchema, ZodError } from 'zod'
|
import type { AnyZodObject, ZodSchema, ZodError } from 'zod'
|
||||||
import { z, ZodType } 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']
|
type RequestPart<R extends RouteConfig, Part extends string> = Part extends keyof R['request']
|
||||||
? R['request'][Part]
|
? R['request'][Part]
|
||||||
: never
|
: {}
|
||||||
|
|
||||||
type InputTypeBase<
|
type InputTypeBase<
|
||||||
R extends RouteConfig,
|
R extends RouteConfig,
|
||||||
|
@ -49,6 +49,7 @@ type InputTypeBase<
|
||||||
> = R['request'] extends RequestTypes
|
> = R['request'] extends RequestTypes
|
||||||
? RequestPart<R, Part> extends AnyZodObject
|
? RequestPart<R, Part> extends AnyZodObject
|
||||||
? {
|
? {
|
||||||
|
in: { [K in Type]: z.input<RequestPart<R, Part>> }
|
||||||
out: { [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>
|
: 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: {
|
out: {
|
||||||
json: z.input<
|
json: z.input<
|
||||||
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
|
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>
|
: 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: {
|
out: {
|
||||||
form: z.input<
|
form: z.input<
|
||||||
R['request']['body']['content'][keyof R['request']['body']['content']]['schema']
|
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,
|
route: R,
|
||||||
handler: Handler<E, R['path'], I, OutputType<R>>,
|
handler: Handler<E, R['path'], I, OutputType<R>>,
|
||||||
hook?: Hook<I, E, R['path'], 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)
|
this.#registry.registerPath(route)
|
||||||
|
|
||||||
const validators: MiddlewareHandler[] = []
|
const validators: MiddlewareHandler[] = []
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
// eslint-disable-next-line node/no-extraneous-import
|
/* eslint-disable node/no-extraneous-import */
|
||||||
import { describe, it, expect } from 'vitest'
|
/* 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'
|
import { OpenAPIHono, createRoute, z } from '../src'
|
||||||
|
|
||||||
describe('Basic - params', () => {
|
describe('Basic - params', () => {
|
||||||
|
@ -338,7 +342,7 @@ describe('Form', () => {
|
||||||
schema: PostsSchema,
|
schema: PostsSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: 'Get the posts',
|
description: 'Post the post',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -382,3 +386,75 @@ describe('Form', () => {
|
||||||
expect(res.status).toBe(400)
|
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