diff --git a/src/context.ts b/src/context.ts index a5f6b4c6a..e27b0456f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -862,4 +862,11 @@ export class Context< this.#notFoundHandler ??= () => new Response() return this.#notFoundHandler(this) } + + validateData: unknown + + validate = (data: T): T => { + this.validateData = data + return data + } } diff --git a/src/types.ts b/src/types.ts index 42b0d3aa2..fc3f10981 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1933,6 +1933,26 @@ export type ValidationTargets } +//////////////////////////////////////////////// +////// ///// +////// ResponseValidationTargets ///// +////// ///// +//////////////////////////////////////////////// + +export type ResponseValidationTargets = { + body: ReadableStream | null + text: string + json: any + html: string + header: Record + cookie: Record + status: { + ok: boolean + status: StatusCode + statusText: string + } +} + //////////////////////////////////////// ////// ////// ////// Path parameters ////// diff --git a/src/validator/index.ts b/src/validator/index.ts index 30e10c678..0197bdaf3 100644 --- a/src/validator/index.ts +++ b/src/validator/index.ts @@ -5,3 +5,6 @@ export { validator } from './validator' export type { ValidationFunction } from './validator' + +export { responseValidator } from './response-validator' +export type { ResponseValidationFunction } from './response-validator' diff --git a/src/validator/response-validator.sandbox.ts b/src/validator/response-validator.sandbox.ts new file mode 100644 index 000000000..88e3e5c96 --- /dev/null +++ b/src/validator/response-validator.sandbox.ts @@ -0,0 +1,30 @@ +import { Hono } from '../preset/quick' +import { validator, responseValidator } from '.' + +const app = new Hono<{ + Bindings: { + foo: string + } +}>() + +app + .get( + '*', + validator('query', (value, c) => { + console.log(value, c) + }), + responseValidator('text', (value, c) => { + console.log(value) + if (!/\d/.test(value)) { + return c.text('Invalid!', 400) + } + }) + ) + .get('/', (c) => { + return c.text(c.validate('hello world at ' + Date.now())) + }) + .get('/invalid', (c) => { + return c.text(c.validate('hello world at ' + NaN)) + }) + +export default app diff --git a/src/validator/response-validator.ts b/src/validator/response-validator.ts new file mode 100644 index 000000000..17156085d --- /dev/null +++ b/src/validator/response-validator.ts @@ -0,0 +1,90 @@ +import type { Context } from '../context' +import type { Env, MiddlewareHandler, ResponseValidationTargets } from '../types' + +type ResponseValidationTargetKeys = keyof ResponseValidationTargets + +export type ResponseValidationFunction< + U extends ResponseValidationTargetKeys, + E extends Env = {}, + P extends string = string +> = ( + value: ResponseValidationTargets[U], + c: Context +) => undefined | Response | Promise + +const textRegex = /^text\/([a-z-\.]+\+)?(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/ +const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/ +const htmlRegex = /^text\/html(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/ + +export const responseValidator = < + P extends string, + U extends ResponseValidationTargetKeys, + P2 extends string = P, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + E extends Env = any +>( + target: U, + validationFunc: ResponseValidationFunction +): MiddlewareHandler => { + return async (c, next) => { + await next() + + if (!c.finalized) { + return + } + + let value: unknown + + const contentType = c.res.headers.get('Content-Type') + + switch (target) { + case 'body': + if (!c.res.body) { + break + } + value = c.res.body + break + case 'text': + if (!contentType || !textRegex.test(contentType) || typeof c.validateData !== 'string') { + break + } + value = c.validateData + break + case 'json': + if (!contentType || !jsonRegex.test(contentType) || typeof c.validateData !== 'object') { + break + } + value = c.validateData + break + case 'html': + if (!contentType || !htmlRegex.test(contentType) || typeof c.validateData !== 'string') { + break + } + value = c.validateData + break + case 'header': + value = Object.fromEntries(c.res.headers.entries()) as Record + break + case 'cookie': + value = c.res.headers.getSetCookie().reduce((record, cookie) => { + const [name, ...rest] = cookie.split('=') + record[name] = rest.join('=').split(';')[0] + return record + }, {} as Record) + break + case 'status': + value = { + ok: c.res.ok, + status: c.res.status, + statusText: c.res.statusText, + } + break + } + + const res = await validationFunc(value, c) + + if (res instanceof Response) { + c.res = res + } + } +}