import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono' import { validator } from 'hono/validator' import type { IReadableURLSearchParams, IValidation } from 'typia' interface IFailure { success: false errors: IValidation.IError[] data: T } type BaseType = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends symbol ? symbol : T extends bigint ? bigint : T type Parsed = T extends Record ? { [K in keyof T]-?: T[K] extends (infer U)[] ? (BaseType | null | undefined)[] | undefined : BaseType | null | undefined } : BaseType export type QueryValidation = any> = ( input: string | URLSearchParams ) => IValidation export type QueryOutputType = T extends QueryValidation ? O : never type QueryStringify = T extends Record ? { // Suppress to split union types [K in keyof T]: [T[K]] extends [bigint | number | boolean] ? `${T[K]}` : T[K] extends (infer U)[] ? [U] extends [bigint | number | boolean] ? `${U}`[] : T[K] : T[K] } : T export type HeaderValidation = any> = ( input: Record ) => IValidation export type HeaderOutputType = T extends HeaderValidation ? O : never type HeaderStringify = T extends Record ? { // Suppress to split union types [K in keyof T]: [T[K]] extends [bigint | number | boolean] ? `${T[K]}` : T[K] extends (infer U)[] ? [U] extends [bigint | number | boolean] ? `${U}` : U : T[K] } : T export type HttpHook = ( result: IValidation.ISuccess | IFailure>, c: Context ) => Response | Promise | void | Promise | TypedResponse export type Hook = ( result: IValidation.ISuccess | IFailure, c: Context ) => Response | Promise | void | Promise | TypedResponse // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Validation = (input: unknown) => IValidation export type OutputType = T extends Validation ? O : never interface TypiaValidator { < T extends QueryValidation, O extends QueryOutputType, E extends Env, P extends string, V extends { in: { query: QueryStringify }; out: { query: O } } = { in: { query: QueryStringify } out: { query: O } } >( target: 'query', validate: T, hook?: HttpHook ): MiddlewareHandler < T extends HeaderValidation, O extends HeaderOutputType, E extends Env, P extends string, V extends { in: { header: HeaderStringify }; out: { header: O } } = { in: { header: HeaderStringify } out: { header: O } } >( target: 'header', validate: T, hook?: HttpHook ): MiddlewareHandler < T extends Validation, O extends OutputType, Target extends Exclude, E extends Env, P extends string, V extends { in: { [K in Target]: O } out: { [K in Target]: O } } = { in: { [K in Target]: O } out: { [K in Target]: O } } >( target: Target, validate: T, hook?: Hook ): MiddlewareHandler } export const typiaValidator: TypiaValidator = ( target: keyof ValidationTargets, validate: (input: any) => IValidation, hook?: Hook ): MiddlewareHandler => { if (target === 'query' || target === 'header') { return async (c, next) => { let value: any if (target === 'query') { const queries = c.req.queries() value = { get: (key) => queries[key]?.[0] ?? null, getAll: (key) => queries[key] ?? [], } satisfies IReadableURLSearchParams } else { value = Object.create(null) for (const [key, headerValue] of c.req.raw.headers) { value[key.toLowerCase()] = headerValue } if (c.req.raw.headers.has('Set-Cookie')) { value['Set-Cookie'] = c.req.raw.headers.getSetCookie() } } const result = validate(value) if (hook) { const res = await hook(result as never, c) if (res instanceof Response) { return res } } if (!result.success) { return c.json({ success: false, error: result.errors }, 400) } c.req.addValidatedData(target, result.data) await next() } } return validator(target, async (value, c) => { const result = validate(value) if (hook) { const hookResult = await hook({ ...result, data: value }, c) if (hookResult) { if (hookResult instanceof Response || hookResult instanceof Promise) { return hookResult } if ('response' in hookResult) { return hookResult.response } } } if (!result.success) { return c.json({ success: false, error: result.errors }, 400) } return result.data }) }