fix(zod-openapi): return `Response` if response is not text or JSON (#853)

* fix(zod-openapi): return `Response` if response is not text or JSON

Co-authored-by: sushichan044 <mail@sushichan.live>

* fixed tests and correct types

* add changeset

---------

Co-authored-by: sushichan044 <mail@sushichan.live>
pull/854/head
Yusuke Wada 2024-11-28 18:35:21 +09:00 committed by GitHub
parent a2ffc34b31
commit a9804afe71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 62 additions and 12 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/zod-openapi': patch
---
fix: return `Response` if response is not text or JSON

View File

@ -26,7 +26,7 @@ import type {
ValidationTargets, ValidationTargets,
} from 'hono' } from 'hono'
import type { MergePath, MergeSchemaPath } from 'hono/types' import type { MergePath, MergeSchemaPath } from 'hono/types'
import type { JSONParsed, RemoveBlankRecord } from 'hono/utils/types' import type { JSONParsed, JSONValue, RemoveBlankRecord, SimplifyDeepArray } from 'hono/utils/types'
import type { import type {
ClientErrorStatusCode, ClientErrorStatusCode,
InfoStatusCode, InfoStatusCode,
@ -69,6 +69,28 @@ type IsForm<T> = T extends string
: never : never
: never : never
type ReturnJsonOrTextOrResponse<
ContentType,
Content,
Status extends keyof StatusCodeRangeDefinitions | StatusCode
> = ContentType extends string
? ContentType extends `application/${infer Start}json${infer _End}`
? Start extends '' | `${string}+` | `vnd.${string}+`
? TypedResponse<
SimplifyDeepArray<Content> extends JSONValue
? JSONValue extends SimplifyDeepArray<Content>
? never
: JSONParsed<Content>
: never,
ExtractStatusCode<Status>,
'json'
>
: never
: ContentType extends `text/plain${infer _Rest}`
? TypedResponse<Content, ExtractStatusCode<Status>, 'text'>
: Response
: never
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]
: {} : {}
@ -173,14 +195,13 @@ type ExtractStatusCode<T extends RouteConfigStatusCode> = T extends keyof Status
? StatusCodeRangeDefinitions[T] ? StatusCodeRangeDefinitions[T]
: T : T
export type RouteConfigToTypedResponse<R extends RouteConfig> = { export type RouteConfigToTypedResponse<R extends RouteConfig> = {
[Status in keyof R['responses'] & RouteConfigStatusCode]: IsJson< [Status in keyof R['responses'] &
keyof R['responses'][Status]['content'] RouteConfigStatusCode]: undefined extends R['responses'][Status]['content']
> extends never
? TypedResponse<{}, ExtractStatusCode<Status>, string> ? TypedResponse<{}, ExtractStatusCode<Status>, string>
: TypedResponse< : ReturnJsonOrTextOrResponse<
JSONParsed<ExtractContent<R['responses'][Status]['content']>>, keyof R['responses'][Status]['content'],
ExtractStatusCode<Status>, ExtractContent<R['responses'][Status]['content']>,
'json' | 'text' Status
> >
}[keyof R['responses'] & RouteConfigStatusCode] }[keyof R['responses'] & RouteConfigStatusCode]

View File

@ -123,4 +123,28 @@ describe('supports async handler', () => {
> >
> >
}) })
test('Route accepts a response other than the response from c.json() and c.text()', () => {
const route = createRoute({
method: 'get',
path: '/html',
responses: {
200: {
content: {
'text/html': {
schema: z.string(),
},
},
description: 'Return HTML',
},
},
})
const handler: RouteHandler<typeof route> = (c) => {
return c.html('<h1>Hello from html</h1>')
}
const hono = new OpenAPIHono()
hono.openapi(route, handler)
})
}) })

View File

@ -76,7 +76,7 @@ describe('Types', () => {
id: number id: number
message: string message: string
} }
outputFormat: 'json' | 'text' outputFormat: 'json'
status: 200 status: 200
} }
} }

View File

@ -1779,21 +1779,21 @@ describe('RouteConfigToTypedResponse', () => {
age: number age: number
}, },
200, 200,
'json' | 'text' 'json'
> >
| TypedResponse< | TypedResponse<
{ {
ok: boolean ok: boolean
}, },
400, 400,
'json' | 'text' 'json'
> >
| TypedResponse< | TypedResponse<
{ {
ok: boolean ok: boolean
}, },
ServerErrorStatusCode, ServerErrorStatusCode,
'json' | 'text' 'json'
> >
type verify = Expect<Equal<Expected, Actual>> type verify = Expect<Equal<Expected, Actual>>
}) })