Skip to content

Commit

Permalink
feat(hono/context): contentful status code typing
Browse files Browse the repository at this point in the history
  • Loading branch information
askorupskyy committed Dec 22, 2024
1 parent 4eb101d commit 993739f
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 35 deletions.
37 changes: 20 additions & 17 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from './types'
import type { ResponseHeader } from './utils/headers'
import { HtmlEscapedCallbackPhase, resolveCallback } from './utils/html'
import type { RedirectStatusCode, StatusCode } from './utils/http-status'
import type { ContentfulStatusCode, RedirectStatusCode, StatusCode } from './utils/http-status'
import type { BaseMime } from './utils/mime'
import type {
InvalidJSONValue,
Expand Down Expand Up @@ -114,7 +114,12 @@ interface NewResponse {
/**
* Interface for responding with a body.
*/
interface BodyRespond extends NewResponse {}
interface BodyRespond {
// if we return content, only allow the status codes that allow for returning the body
(data: Data, status?: ContentfulStatusCode, headers?: HeaderRecord): Response
(data: null, status?: StatusCode, headers?: HeaderRecord): Response
(data: Data | null, init?: ResponseInit): Response
}

/**
* Interface for responding with text.
Expand All @@ -130,13 +135,15 @@ interface BodyRespond extends NewResponse {}
* @returns {Response & TypedResponse<T, U, 'text'>} - The response after rendering the text content, typed with the provided text and status code types.
*/
interface TextRespond {
<T extends string, U extends StatusCode = StatusCode>(
<T extends string, U extends ContentfulStatusCode = ContentfulStatusCode>(
text: T,
status?: U,
headers?: HeaderRecord
): Response & TypedResponse<T, U, 'text'>
<T extends string, U extends StatusCode = StatusCode>(text: T, init?: ResponseInit): Response &
TypedResponse<T, U, 'text'>
<T extends string, U extends ContentfulStatusCode = ContentfulStatusCode>(
text: T,
init?: ResponseInit
): Response & TypedResponse<T, U, 'text'>
}

/**
Expand All @@ -155,15 +162,15 @@ interface TextRespond {
interface JSONRespond {
<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
U extends ContentfulStatusCode = ContentfulStatusCode
>(
object: T,
status?: U,
headers?: HeaderRecord
): JSONRespondReturn<T, U>
<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
U extends ContentfulStatusCode = ContentfulStatusCode
>(
object: T,
init?: ResponseInit
Expand All @@ -178,7 +185,7 @@ interface JSONRespond {
*/
type JSONRespondReturn<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode
U extends ContentfulStatusCode
> = Response &
TypedResponse<
SimplifyDeepArray<T> extends JSONValue
Expand All @@ -203,7 +210,7 @@ type JSONRespondReturn<
interface HTMLRespond {
<T extends string | Promise<string>>(
html: T,
status?: StatusCode,
status?: ContentfulStatusCode,
headers?: HeaderRecord
): T extends string ? Response : Promise<Response>
<T extends string | Promise<string>>(html: T, init?: ResponseInit): T extends string
Expand Down Expand Up @@ -712,11 +719,7 @@ export class Context<
* })
* ```
*/
body: BodyRespond = (
data: Data | null,
arg?: StatusCode | ResponseInit,
headers?: HeaderRecord
): Response => {
body: BodyRespond = (data, arg?: StatusCode | RequestInit, headers?: HeaderRecord) => {
return typeof arg === 'number'
? this.#newResponse(data, arg, headers)
: this.#newResponse(data, arg)
Expand All @@ -736,7 +739,7 @@ export class Context<
*/
text: TextRespond = (
text: string,
arg?: StatusCode | ResponseInit,
arg?: ContentfulStatusCode | ResponseInit,
headers?: HeaderRecord
): ReturnType<TextRespond> => {
// If the header is empty, return Response immediately.
Expand Down Expand Up @@ -769,7 +772,7 @@ export class Context<
*/
json: JSONRespond = <
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
U extends ContentfulStatusCode = ContentfulStatusCode
>(
object: T,
arg?: U | ResponseInit,
Expand All @@ -786,7 +789,7 @@ export class Context<

html: HTMLRespond = (
html: string | Promise<string>,
arg?: StatusCode | ResponseInit,
arg?: ContentfulStatusCode | ResponseInit,
headers?: HeaderRecord
): Response | Promise<Response> => {
this.#preparedHeaders ??= {}
Expand Down
55 changes: 37 additions & 18 deletions src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
ToSchema,
TypedResponse,
} from './types'
import type { StatusCode } from './utils/http-status'
import type { ContentfulStatusCode, StatusCode } from './utils/http-status'
import type { Equal, Expect } from './utils/types'
import { validator } from './validator'

Expand Down Expand Up @@ -96,7 +96,7 @@ describe('HandlerInterface', () => {
message: string
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -135,7 +135,7 @@ describe('HandlerInterface', () => {
message: string
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -163,7 +163,7 @@ describe('HandlerInterface', () => {
}
output: 'foo'
outputFormat: 'text'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -192,7 +192,7 @@ describe('HandlerInterface', () => {
}
output: string
outputFormat: 'text'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand All @@ -217,7 +217,7 @@ describe('HandlerInterface', () => {
}
output: string
outputFormat: 'text'
status: StatusCode
status: ContentfulStatusCode
}
}
} & {
Expand Down Expand Up @@ -271,7 +271,7 @@ describe('OnHandlerInterface', () => {
success: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -376,7 +376,7 @@ describe('Support c.json(undefined)', () => {
input: {}
output: never
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -460,7 +460,7 @@ describe('`json()`', () => {
message: string
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -902,23 +902,23 @@ describe('Different types using json()', () => {
ng: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
| {
input: {}
output: {
ok: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
| {
input: {}
output: {
default: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -974,7 +974,7 @@ describe('Different types using json()', () => {
default: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -1012,23 +1012,23 @@ describe('Different types using json()', () => {
ng: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
| {
input: {}
output: {
ok: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
| {
input: {}
output: {
default: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -1084,7 +1084,7 @@ describe('Different types using json()', () => {
default: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand All @@ -1111,7 +1111,7 @@ describe('json() in an async handler', () => {
ok: boolean
}
outputFormat: 'json'
status: StatusCode
status: ContentfulStatusCode
}
}
}
Expand Down Expand Up @@ -2271,3 +2271,22 @@ describe('generic typed variables', () => {
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})
describe('status code', () => {
const app = new Hono()

it('should only allow to return .json() with contentful status codes', async () => {
const route = app.get('/', async (c) => c.json({}))
type Actual = ExtractSchema<typeof route>['/']['$get']['status']
expectTypeOf<Actual>().toEqualTypeOf<ContentfulStatusCode>()
})
it('should only allow to return .body(null) with all status codes', async () => {
const route = app.get('/', async (c) => c.body(null))
type Actual = ExtractSchema<typeof route>['/']['$get']['status']
expectTypeOf<Actual>().toEqualTypeOf<StatusCode>()
})
it('should only allow to return .text() with contentful status codes', async () => {
const route = app.get('/', async (c) => c.text('whatever'))
type Actual = ExtractSchema<typeof route>['/']['$get']['status']
expectTypeOf<Actual>().toEqualTypeOf<ContentfulStatusCode>()
})
})
3 changes: 3 additions & 0 deletions src/utils/http-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ export type StatusCode =
| ClientErrorStatusCode
| ServerErrorStatusCode
| UnofficialStatusCode

export type ContentlessStatusCode = 101 | 204 | 205 | 304
export type ContentfulStatusCode = Exclude<StatusCode, ContentlessStatusCode>

0 comments on commit 993739f

Please sign in to comment.