-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(typia-validator): support typia http module (#888)
* feature(typia-validator): support typia http module * feature(typia-validator): add change-set & update README
- Loading branch information
Showing
10 changed files
with
860 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
--- | ||
'@hono/typia-validator': minor | ||
--- | ||
|
||
Enables handling of `number`, `boolean`, and `bigint` types in query parameters and headers. | ||
|
||
```diff | ||
- import { typiaValidator } from '@hono/typia-validator'; | ||
+ import { typiaValidator } from '@hono/typia-validator/http'; | ||
import { Hono } from 'hono'; | ||
import typia, { type tags } from 'typia'; | ||
|
||
interface Schema { | ||
- pages: `${number}`[]; | ||
+ pages: (number & tags.Type<'uint32'>)[]; | ||
} | ||
|
||
const app = new Hono() | ||
.get( | ||
'/books', | ||
typiaValidator( | ||
- typia.createValidate<Schema>(), | ||
+ typia.http.createValidateQuery<Schema>(), | ||
async (result, c) => { | ||
if (!result.success) | ||
return c.text('Invalid query parameters', 400); | ||
- return { pages: result.data.pages.map(Number) }; | ||
} | ||
), | ||
async c => { | ||
const { pages } = c.req.valid('query'); // { pages: number[] } | ||
//... | ||
} | ||
) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/test-generated |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// @ts-check | ||
const fs = require('node:fs') | ||
const path = require('node:path') | ||
|
||
// https://github.com/samchon/typia/issues/1432 | ||
// typia generated files have some type errors | ||
|
||
const generatedFiles = fs | ||
.readdirSync(path.resolve(__dirname, '../test-generated')) | ||
.map((file) => path.resolve(__dirname, '../test-generated', file)) | ||
|
||
for (const file of generatedFiles) { | ||
const content = fs.readFileSync(file, 'utf8') | ||
const lines = content.split('\n') | ||
const distLines = [] | ||
for (const line of lines) { | ||
if ( | ||
line.includes('._httpHeaderReadNumber(') || | ||
line.includes('._httpHeaderReadBigint(') || | ||
line.includes('._httpHeaderReadBoolean(') | ||
) | ||
distLines.push(`// @ts-ignore`) | ||
distLines.push(line) | ||
} | ||
|
||
fs.writeFileSync(file, distLines.join('\n')) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono' | ||
import { validator } from 'hono/validator' | ||
import type { IReadableURLSearchParams, IValidation } from 'typia' | ||
|
||
interface IFailure<T> { | ||
success: false | ||
errors: IValidation.IError[] | ||
data: T | ||
} | ||
|
||
type BaseType<T> = T extends string | ||
? string | ||
: T extends number | ||
? number | ||
: T extends boolean | ||
? boolean | ||
: T extends symbol | ||
? symbol | ||
: T extends bigint | ||
? bigint | ||
: T | ||
type Parsed<T> = T extends Record<string | number, any> | ||
? { | ||
[K in keyof T]-?: T[K] extends (infer U)[] | ||
? (BaseType<U> | null | undefined)[] | undefined | ||
: BaseType<T[K]> | null | undefined | ||
} | ||
: BaseType<T> | ||
|
||
export type QueryValidation<O extends Record<string | number, any> = any> = ( | ||
input: string | URLSearchParams | ||
) => IValidation<O> | ||
export type QueryOutputType<T> = T extends QueryValidation<infer O> ? O : never | ||
type QueryStringify<T> = T extends Record<string | number, any> | ||
? { | ||
// 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<O extends Record<string | number, any> = any> = ( | ||
input: Record<string, string | string[] | undefined> | ||
) => IValidation<O> | ||
export type HeaderOutputType<T> = T extends HeaderValidation<infer O> ? O : never | ||
type HeaderStringify<T> = T extends Record<string | number, any> | ||
? { | ||
// 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<T, E extends Env, P extends string, O = {}> = ( | ||
result: IValidation.ISuccess<T> | IFailure<Parsed<T>>, | ||
c: Context<E, P> | ||
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<O> | ||
export type Hook<T, E extends Env, P extends string, O = {}> = ( | ||
result: IValidation.ISuccess<T> | IFailure<T>, | ||
c: Context<E, P> | ||
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<O> | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type Validation<O = any> = (input: unknown) => IValidation<O> | ||
export type OutputType<T> = T extends Validation<infer O> ? O : never | ||
|
||
interface TypiaValidator { | ||
< | ||
T extends QueryValidation, | ||
O extends QueryOutputType<T>, | ||
E extends Env, | ||
P extends string, | ||
V extends { in: { query: QueryStringify<O> }; out: { query: O } } = { | ||
in: { query: QueryStringify<O> } | ||
out: { query: O } | ||
} | ||
>( | ||
target: 'query', | ||
validate: T, | ||
hook?: HttpHook<O, E, P> | ||
): MiddlewareHandler<E, P, V> | ||
|
||
< | ||
T extends HeaderValidation, | ||
O extends HeaderOutputType<T>, | ||
E extends Env, | ||
P extends string, | ||
V extends { in: { header: HeaderStringify<O> }; out: { header: O } } = { | ||
in: { header: HeaderStringify<O> } | ||
out: { header: O } | ||
} | ||
>( | ||
target: 'header', | ||
validate: T, | ||
hook?: HttpHook<O, E, P> | ||
): MiddlewareHandler<E, P, V> | ||
|
||
< | ||
T extends Validation, | ||
O extends OutputType<T>, | ||
Target extends Exclude<keyof ValidationTargets, 'query' | 'queries' | 'header'>, | ||
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<O, E, P> | ||
): MiddlewareHandler<E, P, V> | ||
} | ||
|
||
export const typiaValidator: TypiaValidator = ( | ||
target: keyof ValidationTargets, | ||
validate: (input: any) => IValidation<any>, | ||
hook?: Hook<any, any, any> | ||
): 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 | ||
}) | ||
} |
Oops, something went wrong.