From 49f5d3e469bc1d9f9aca6d498f6df1602b91fa52 Mon Sep 17 00:00:00 2001 From: askorupskyy <50280805+rcbxd@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:16:15 -0600 Subject: [PATCH] feat(hono/context): contentful status code typing --- src/context.ts | 37 ++++++++++--------- src/helper/factory/index.test.ts | 4 +-- src/http-exception.ts | 6 ++-- src/middleware/bearer-auth/index.ts | 4 +-- src/types.test.ts | 55 +++++++++++++++++++---------- src/utils/http-status.ts | 3 ++ src/validator/validator.test.ts | 16 ++++----- 7 files changed, 75 insertions(+), 50 deletions(-) diff --git a/src/context.ts b/src/context.ts index 8447f2128..f5462be29 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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, @@ -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. @@ -130,13 +135,15 @@ interface BodyRespond extends NewResponse {} * @returns {Response & TypedResponse} - The response after rendering the text content, typed with the provided text and status code types. */ interface TextRespond { - ( + ( text: T, status?: U, headers?: HeaderRecord ): Response & TypedResponse - (text: T, init?: ResponseInit): Response & - TypedResponse + ( + text: T, + init?: ResponseInit + ): Response & TypedResponse } /** @@ -155,7 +162,7 @@ interface TextRespond { interface JSONRespond { < T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, - U extends StatusCode = StatusCode + U extends ContentfulStatusCode = ContentfulStatusCode >( object: T, status?: U, @@ -163,7 +170,7 @@ interface JSONRespond { ): JSONRespondReturn < T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, - U extends StatusCode = StatusCode + U extends ContentfulStatusCode = ContentfulStatusCode >( object: T, init?: ResponseInit @@ -178,7 +185,7 @@ interface JSONRespond { */ type JSONRespondReturn< T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, - U extends StatusCode + U extends ContentfulStatusCode > = Response & TypedResponse< SimplifyDeepArray extends JSONValue @@ -203,7 +210,7 @@ type JSONRespondReturn< interface HTMLRespond { >( html: T, - status?: StatusCode, + status?: ContentfulStatusCode, headers?: HeaderRecord ): T extends string ? Response : Promise >(html: T, init?: ResponseInit): T extends string @@ -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) @@ -736,7 +739,7 @@ export class Context< */ text: TextRespond = ( text: string, - arg?: StatusCode | ResponseInit, + arg?: ContentfulStatusCode | ResponseInit, headers?: HeaderRecord ): ReturnType => { // If the header is empty, return Response immediately. @@ -769,7 +772,7 @@ export class Context< */ json: JSONRespond = < T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, - U extends StatusCode = StatusCode + U extends ContentfulStatusCode = ContentfulStatusCode >( object: T, arg?: U | ResponseInit, @@ -786,7 +789,7 @@ export class Context< html: HTMLRespond = ( html: string | Promise, - arg?: StatusCode | ResponseInit, + arg?: ContentfulStatusCode | ResponseInit, headers?: HeaderRecord ): Response | Promise => { this.#preparedHeaders ??= {} diff --git a/src/helper/factory/index.test.ts b/src/helper/factory/index.test.ts index 30fe77b61..a6eeefca6 100644 --- a/src/helper/factory/index.test.ts +++ b/src/helper/factory/index.test.ts @@ -4,7 +4,7 @@ import { hc } from '../../client' import type { ClientRequest } from '../../client/types' import { Hono } from '../../index' import type { ToSchema, TypedResponse } from '../../types' -import type { StatusCode } from '../../utils/http-status' +import type { ContentfulStatusCode, StatusCode } from '../../utils/http-status' import { validator } from '../../validator' import { createFactory, createMiddleware } from './index' @@ -106,7 +106,7 @@ describe('createHandler', () => { input: {} output: 'A' outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } }> }>() diff --git a/src/http-exception.ts b/src/http-exception.ts index f7ce30526..8fe9c2bb3 100644 --- a/src/http-exception.ts +++ b/src/http-exception.ts @@ -3,7 +3,7 @@ * This module provides the `HTTPException` class. */ -import type { StatusCode } from './utils/http-status' +import type { ContentfulStatusCode } from './utils/http-status' /** * Options for creating an `HTTPException`. @@ -45,14 +45,14 @@ type HTTPExceptionOptions = { */ export class HTTPException extends Error { readonly res?: Response - readonly status: StatusCode + readonly status: ContentfulStatusCode /** * Creates an instance of `HTTPException`. * @param status - HTTP status code for the exception. Defaults to 500. * @param options - Additional options for the exception. */ - constructor(status: StatusCode = 500, options?: HTTPExceptionOptions) { + constructor(status: ContentfulStatusCode = 500, options?: HTTPExceptionOptions) { super(options?.message, { cause: options?.cause }) this.res = options?.res this.status = status diff --git a/src/middleware/bearer-auth/index.ts b/src/middleware/bearer-auth/index.ts index 7a83345f4..0388e5e65 100644 --- a/src/middleware/bearer-auth/index.ts +++ b/src/middleware/bearer-auth/index.ts @@ -7,7 +7,7 @@ import type { Context } from '../../context' import { HTTPException } from '../../http-exception' import type { MiddlewareHandler } from '../../types' import { timingSafeEqual } from '../../utils/buffer' -import type { StatusCode } from '../../utils/http-status' +import type { ContentfulStatusCode } from '../../utils/http-status' const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*' const PREFIX = 'Bearer' @@ -87,7 +87,7 @@ export const bearerAuth = (options: BearerAuthOptions): MiddlewareHandler => { const throwHTTPException = async ( c: Context, - status: StatusCode, + status: ContentfulStatusCode, wwwAuthenticateHeader: string, messageOption: string | object | MessageFunction ): Promise => { diff --git a/src/types.test.ts b/src/types.test.ts index 421570abe..5118d3212 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -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' @@ -96,7 +96,7 @@ describe('HandlerInterface', () => { message: string } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -135,7 +135,7 @@ describe('HandlerInterface', () => { message: string } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -163,7 +163,7 @@ describe('HandlerInterface', () => { } output: 'foo' outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } } } @@ -192,7 +192,7 @@ describe('HandlerInterface', () => { } output: string outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } } } @@ -217,7 +217,7 @@ describe('HandlerInterface', () => { } output: string outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } } } & { @@ -271,7 +271,7 @@ describe('OnHandlerInterface', () => { success: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -376,7 +376,7 @@ describe('Support c.json(undefined)', () => { input: {} output: never outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -460,7 +460,7 @@ describe('`json()`', () => { message: string } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -902,7 +902,7 @@ describe('Different types using json()', () => { ng: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } | { input: {} @@ -910,7 +910,7 @@ describe('Different types using json()', () => { ok: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } | { input: {} @@ -918,7 +918,7 @@ describe('Different types using json()', () => { default: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -974,7 +974,7 @@ describe('Different types using json()', () => { default: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -1012,7 +1012,7 @@ describe('Different types using json()', () => { ng: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } | { input: {} @@ -1020,7 +1020,7 @@ describe('Different types using json()', () => { ok: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } | { input: {} @@ -1028,7 +1028,7 @@ describe('Different types using json()', () => { default: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -1084,7 +1084,7 @@ describe('Different types using json()', () => { default: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -1111,7 +1111,7 @@ describe('json() in an async handler', () => { ok: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -2271,3 +2271,22 @@ describe('generic typed variables', () => { expectTypeOf().toEqualTypeOf() }) }) +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['/']['$get']['status'] + expectTypeOf().toEqualTypeOf() + }) + 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['/']['$get']['status'] + expectTypeOf().toEqualTypeOf() + }) + it('should only allow to return .text() with contentful status codes', async () => { + const route = app.get('/', async (c) => c.text('whatever')) + type Actual = ExtractSchema['/']['$get']['status'] + expectTypeOf().toEqualTypeOf() + }) +}) diff --git a/src/utils/http-status.ts b/src/utils/http-status.ts index 6882394f4..07610a39b 100644 --- a/src/utils/http-status.ts +++ b/src/utils/http-status.ts @@ -67,3 +67,6 @@ export type StatusCode = | ClientErrorStatusCode | ServerErrorStatusCode | UnofficialStatusCode + +export type ContentlessStatusCode = 101 | 204 | 205 | 304 +export type ContentfulStatusCode = Exclude diff --git a/src/validator/validator.test.ts b/src/validator/validator.test.ts index 05ff812fd..40e214f9b 100644 --- a/src/validator/validator.test.ts +++ b/src/validator/validator.test.ts @@ -10,7 +10,7 @@ import type { ParsedFormValue, ValidationTargets, } from '../types' -import type { StatusCode } from '../utils/http-status' +import type { ContentfulStatusCode } from '../utils/http-status' import type { Equal, Expect } from '../utils/types' import type { ValidationFunction } from './validator' import { validator } from './validator' @@ -65,7 +65,7 @@ describe('Basic', () => { } output: 'Valid!' outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } } } @@ -477,7 +477,7 @@ describe('Validator middleware with a custom validation function', () => { } } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -540,7 +540,7 @@ describe('Validator middleware with Zod validates JSON', () => { } } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -829,7 +829,7 @@ describe('Validator middleware with Zod multiple validators', () => { title: string } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -893,7 +893,7 @@ it('With path parameters', () => { } output: 'Valid!' outputFormat: 'text' - status: StatusCode + status: ContentfulStatusCode } } } @@ -941,7 +941,7 @@ it('`on`', () => { success: boolean } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } } @@ -1198,7 +1198,7 @@ describe('Transform', () => { page: number } outputFormat: 'json' - status: StatusCode + status: ContentfulStatusCode } } }