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,
} from 'hono'
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 {
ClientErrorStatusCode,
InfoStatusCode,
@ -69,6 +69,28 @@ type IsForm<T> = T extends string
: 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']
? R['request'][Part]
: {}
@ -173,14 +195,13 @@ type ExtractStatusCode<T extends RouteConfigStatusCode> = T extends keyof Status
? StatusCodeRangeDefinitions[T]
: T
export type RouteConfigToTypedResponse<R extends RouteConfig> = {
[Status in keyof R['responses'] & RouteConfigStatusCode]: IsJson<
keyof R['responses'][Status]['content']
> extends never
[Status in keyof R['responses'] &
RouteConfigStatusCode]: undefined extends R['responses'][Status]['content']
? TypedResponse<{}, ExtractStatusCode<Status>, string>
: TypedResponse<
JSONParsed<ExtractContent<R['responses'][Status]['content']>>,
ExtractStatusCode<Status>,
'json' | 'text'
: ReturnJsonOrTextOrResponse<
keyof R['responses'][Status]['content'],
ExtractContent<R['responses'][Status]['content']>,
Status
>
}[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
message: string
}
outputFormat: 'json' | 'text'
outputFormat: 'json'
status: 200
}
}

View File

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