diff --git a/.changeset/big-students-yawn.md b/.changeset/big-students-yawn.md new file mode 100644 index 00000000..1b681e5c --- /dev/null +++ b/.changeset/big-students-yawn.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': minor +--- + +feat(zod-openapi): support RPC-mode diff --git a/packages/zod-openapi/README.md b/packages/zod-openapi/README.md index e4cb0d92..dfb807ab 100644 --- a/packages/zod-openapi/README.md +++ b/packages/zod-openapi/README.md @@ -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('http://localhost:8787/') +``` + ## References - [Hono](https://hono.dev/) diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index c061610a..8b2ea78f 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -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 extends string type RequestPart = 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 extends AnyZodObject ? { + in: { [K in Type]: z.input> } out: { [K in Type]: z.input> } } : {} @@ -61,6 +62,11 @@ type InputTypeJson = R['request'] extends RequestTypes ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { + 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['request'] extends RequestTypes ? {} : R['request']['body']['content'][keyof R['request']['body']['content']]['schema'] extends ZodSchema ? { + 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>, hook?: Hook> - ) => { + ): Hono>, BasePath> => { this.#registry.registerPath(route) const validators: MiddlewareHandler[] = [] diff --git a/packages/zod-openapi/test/index.test.ts b/packages/zod-openapi/test/index.test.ts index 07e16a18..c3040bbd 100644 --- a/packages/zod-openapi/test/index.test.ts +++ b/packages/zod-openapi/test/index.test.ts @@ -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 + }) +})