mirror of
https://github.com/honojs/hono.git
synced 2024-11-24 11:07:29 +01:00
refactor(types): refactor and add tests for checking Types (#615)
* refactor(types): refactor and add tests for checking Types * remove unused * uncomment * use `Handler` in validator middleware * remove unused * create `src/validator` dir and move some files into it * add the case that the context is in `validator` * rename `D` to `S`
This commit is contained in:
parent
77b0815d22
commit
8627010094
@ -1,16 +1,22 @@
|
||||
import { HonoContext } from './context.ts'
|
||||
import type { Environment, NotFoundHandler, ErrorHandler } from './hono.ts'
|
||||
import type { Schema } from './validator/schema.ts'
|
||||
|
||||
interface ComposeContext {
|
||||
finalized: boolean
|
||||
res: any
|
||||
res: unknown
|
||||
}
|
||||
|
||||
// Based on the code in the MIT licensed `koa-compose` package.
|
||||
export const compose = <C extends ComposeContext, E extends Partial<Environment> = Environment>(
|
||||
export const compose = <
|
||||
C extends ComposeContext,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends Partial<Schema> = Schema
|
||||
>(
|
||||
middleware: Function[],
|
||||
onNotFound?: NotFoundHandler<E>,
|
||||
onError?: ErrorHandler<E>
|
||||
onNotFound?: NotFoundHandler<P, E, D>,
|
||||
onError?: ErrorHandler<P, E, D>
|
||||
) => {
|
||||
const middlewareLength = middleware.length
|
||||
return (context: C, next?: Function) => {
|
||||
|
@ -1,24 +1,19 @@
|
||||
import type {
|
||||
Environment,
|
||||
NotFoundHandler,
|
||||
ContextVariableMap,
|
||||
Bindings,
|
||||
ValidatedData,
|
||||
} from './hono.ts'
|
||||
import type { Environment, NotFoundHandler, ContextVariableMap, Bindings } from './hono.ts'
|
||||
import type { CookieOptions } from './utils/cookie.ts'
|
||||
import { serialize } from './utils/cookie.ts'
|
||||
import type { StatusCode } from './utils/http-status.ts'
|
||||
import type { Schema, SchemaToProp } from './validator/schema.ts'
|
||||
|
||||
type HeaderField = [string, string]
|
||||
type Headers = Record<string, string | string[]>
|
||||
export type Data = string | ArrayBuffer | ReadableStream
|
||||
|
||||
export interface Context<
|
||||
RequestParamKeyType extends string = string,
|
||||
E extends Partial<Environment> = any,
|
||||
D extends ValidatedData = ValidatedData
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> {
|
||||
req: Request<RequestParamKeyType, D>
|
||||
req: Request<P, SchemaToProp<S>>
|
||||
env: E['Bindings']
|
||||
event: FetchEvent
|
||||
executionCtx: ExecutionContext
|
||||
@ -32,7 +27,7 @@ export interface Context<
|
||||
set: {
|
||||
<Key extends keyof ContextVariableMap>(key: Key, value: ContextVariableMap[Key]): void
|
||||
<Key extends keyof E['Variables']>(key: Key, value: E['Variables'][Key]): void
|
||||
(key: string, value: any): void
|
||||
(key: string, value: unknown): void
|
||||
}
|
||||
get: {
|
||||
<Key extends keyof ContextVariableMap>(key: Key): ContextVariableMap[Key]
|
||||
@ -51,12 +46,12 @@ export interface Context<
|
||||
}
|
||||
|
||||
export class HonoContext<
|
||||
RequestParamKeyType extends string = string,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
> implements Context<RequestParamKeyType, E, D>
|
||||
S extends Partial<Schema> = Schema
|
||||
> implements Context<P, E, S>
|
||||
{
|
||||
req: Request<RequestParamKeyType, D>
|
||||
req: Request<P, SchemaToProp<S>>
|
||||
env: E['Bindings']
|
||||
finalized: boolean
|
||||
error: Error | undefined = undefined
|
||||
@ -65,19 +60,19 @@ export class HonoContext<
|
||||
private _executionCtx: FetchEvent | ExecutionContext | undefined
|
||||
private _pretty: boolean = false
|
||||
private _prettySpace: number = 2
|
||||
private _map: Record<string, any> | undefined
|
||||
private _map: Record<string, unknown> | undefined
|
||||
private _headers: Record<string, string[]> | undefined
|
||||
private _res: Response | undefined
|
||||
private notFoundHandler: NotFoundHandler<E>
|
||||
private notFoundHandler: NotFoundHandler<P, E, S>
|
||||
|
||||
constructor(
|
||||
req: Request<RequestParamKeyType>,
|
||||
env: E['Bindings'] | undefined = undefined,
|
||||
req: Request<P>,
|
||||
env: E['Bindings'] = {},
|
||||
executionCtx: FetchEvent | ExecutionContext | undefined = undefined,
|
||||
notFoundHandler: NotFoundHandler<E> = () => new Response()
|
||||
notFoundHandler: NotFoundHandler<P, E, S> = () => new Response()
|
||||
) {
|
||||
this._executionCtx = executionCtx
|
||||
this.req = req as Request<RequestParamKeyType, D>
|
||||
this.req = req as Request<P, SchemaToProp<S>>
|
||||
this.env = env || ({} as Bindings)
|
||||
|
||||
this.notFoundHandler = notFoundHandler
|
||||
@ -142,15 +137,15 @@ export class HonoContext<
|
||||
|
||||
set<Key extends keyof ContextVariableMap>(key: Key, value: ContextVariableMap[Key]): void
|
||||
set<Key extends keyof E['Variables']>(key: Key, value: E['Variables'][Key]): void
|
||||
set(key: string, value: any): void
|
||||
set(key: string, value: any): void {
|
||||
set(key: string, value: unknown): void
|
||||
set(key: string, value: unknown): void {
|
||||
this._map ||= {}
|
||||
this._map[key] = value
|
||||
}
|
||||
|
||||
get<Key extends keyof ContextVariableMap>(key: Key): ContextVariableMap[Key]
|
||||
get<Key extends keyof E['Variables']>(key: Key): E['Variables'][Key]
|
||||
get<T = any>(key: string): T
|
||||
get<T>(key: string): T
|
||||
get(key: string) {
|
||||
if (!this._map) {
|
||||
return undefined
|
||||
@ -233,6 +228,6 @@ export class HonoContext<
|
||||
}
|
||||
|
||||
notFound(): Response | Promise<Response> {
|
||||
return this.notFoundHandler(this as any)
|
||||
return this.notFoundHandler(this)
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { SmartRouter } from './router/smart-router/index.ts'
|
||||
import { StaticRouter } from './router/static-router/index.ts'
|
||||
import { TrieRouter } from './router/trie-router/index.ts'
|
||||
import { getPathFromURL, mergePath } from './utils/url.ts'
|
||||
import type { Schema } from './validator/schema.ts'
|
||||
|
||||
export interface ContextVariableMap {}
|
||||
|
||||
@ -20,30 +21,29 @@ export type Environment = {
|
||||
Variables: Variables
|
||||
}
|
||||
|
||||
export type ValidatedData = Record<string, any> // For validated data
|
||||
|
||||
export type Handler<
|
||||
RequestParamKeyType extends string = string,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
> = (
|
||||
c: Context<RequestParamKeyType, E, D>,
|
||||
next: Next
|
||||
) => Response | Promise<Response> | Promise<void> | Promise<Response | undefined | void>
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>, next: Next) => Response | Promise<Response | undefined | void>
|
||||
|
||||
export type MiddlewareHandler = <E extends Partial<Environment> = Environment>(
|
||||
c: Context<string, E>,
|
||||
next: Next
|
||||
) => Promise<void> | Promise<Response | undefined>
|
||||
export type MiddlewareHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>, next: Next) => Promise<Response | undefined | void>
|
||||
|
||||
export type NotFoundHandler<E extends Partial<Environment> = Environment> = (
|
||||
c: Context<string, E>
|
||||
) => Response | Promise<Response>
|
||||
export type NotFoundHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>) => Response | Promise<Response>
|
||||
|
||||
export type ErrorHandler<E extends Partial<Environment> = Environment> = (
|
||||
err: Error,
|
||||
c: Context<string, E>
|
||||
) => Response
|
||||
export type ErrorHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (err: Error, c: Context<P, E, S>) => Response
|
||||
|
||||
export type Next = () => Promise<void>
|
||||
|
||||
@ -60,52 +60,65 @@ type ParamKeys<Path> = Path extends `${infer Component}/${infer Rest}`
|
||||
? ParamKey<Component> | ParamKeys<Rest>
|
||||
: ParamKey<Path>
|
||||
|
||||
interface HandlerInterface<T extends string, E extends Partial<Environment>, U = Hono<E, T>> {
|
||||
interface HandlerInterface<
|
||||
P extends string,
|
||||
E extends Partial<Environment>,
|
||||
S extends Partial<Schema>,
|
||||
U = Hono<E, P, S>
|
||||
> {
|
||||
// app.get(handler...)
|
||||
<Path extends string, Data extends ValidatedData>(
|
||||
<Path extends string, Data extends Schema>(
|
||||
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E, Data>[]
|
||||
): U
|
||||
(...handlers: Handler<string, E>[]): U
|
||||
(...handlers: Handler<string, E, S>[]): U
|
||||
|
||||
// app.get('/', handler, handler...)
|
||||
<Path extends string, Data extends ValidatedData>(
|
||||
<Path extends string, Data extends Partial<Schema> = Schema>(
|
||||
path: Path,
|
||||
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E, Data>[]
|
||||
): U
|
||||
(path: string, ...handlers: Handler<string, E>[]): U
|
||||
<Path extends string, Data extends Schema>(path: Path, ...handlers: Handler<string, E, Data>[]): U
|
||||
(path: string, ...handlers: Handler<string, E, S>[]): U
|
||||
}
|
||||
|
||||
type Methods = typeof METHODS[number] | typeof METHOD_NAME_ALL_LOWERCASE
|
||||
|
||||
function defineDynamicClass(): {
|
||||
new <E extends Partial<Environment> = Environment, T extends string = string, U = Hono<E, T>>(): {
|
||||
[K in Methods]: HandlerInterface<T, E, U>
|
||||
new <
|
||||
E extends Partial<Environment> = Environment,
|
||||
P extends string = string,
|
||||
S extends Partial<Schema> = Schema,
|
||||
U = Hono<E, P, S>
|
||||
>(): {
|
||||
[K in Methods]: HandlerInterface<P, E, S, U>
|
||||
}
|
||||
} {
|
||||
return class {} as any
|
||||
return class {} as never
|
||||
}
|
||||
|
||||
interface Route<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
S extends Partial<Schema> = Schema
|
||||
> {
|
||||
path: string
|
||||
method: string
|
||||
handler: Handler<string, E, D>
|
||||
handler: Handler<P, E, S>
|
||||
}
|
||||
|
||||
export class Hono<
|
||||
E extends Partial<Environment> = Environment,
|
||||
P extends string = '/',
|
||||
D extends ValidatedData = ValidatedData
|
||||
> extends defineDynamicClass()<E, P, Hono<E, P, D>> {
|
||||
readonly router: Router<Handler<string, E, D>> = new SmartRouter({
|
||||
S extends Partial<Schema> = Schema
|
||||
> extends defineDynamicClass()<E, P, S, Hono<E, P, S>> {
|
||||
readonly router: Router<Handler<P, E, S>> = new SmartRouter({
|
||||
routers: [new StaticRouter(), new RegExpRouter(), new TrieRouter()],
|
||||
})
|
||||
readonly strict: boolean = true // strict routing - default is true
|
||||
private _tempPath: string = ''
|
||||
private path: string = '/'
|
||||
|
||||
routes: Route<E, D>[] = []
|
||||
routes: Route<P, E, S>[] = []
|
||||
|
||||
constructor(init: Partial<Pick<Hono, 'router' | 'strict'>> = {}) {
|
||||
super()
|
||||
@ -114,18 +127,18 @@ export class Hono<
|
||||
|
||||
const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE]
|
||||
allMethods.map((method) => {
|
||||
this[method] = <Path extends string = ''>(
|
||||
args1: Path | Handler<ParamKeys<Path>, E, D>,
|
||||
...args: [Handler<ParamKeys<Path>, E, D>]
|
||||
this[method] = <Path extends string, Env extends Environment, Data extends Schema>(
|
||||
args1: Path | Handler<ParamKeys<Path>, Env, Data>,
|
||||
...args: [Handler<ParamKeys<Path>, Env, Data>]
|
||||
): this => {
|
||||
if (typeof args1 === 'string') {
|
||||
this.path = args1
|
||||
} else {
|
||||
this.addRoute(method, this.path, args1)
|
||||
this.addRoute(method, this.path, args1 as unknown as Handler<P, E, S>)
|
||||
}
|
||||
args.map((handler) => {
|
||||
if (typeof handler !== 'string') {
|
||||
this.addRoute(method, this.path, handler)
|
||||
this.addRoute(method, this.path, handler as unknown as Handler<P, E, S>)
|
||||
}
|
||||
})
|
||||
return this
|
||||
@ -135,17 +148,17 @@ export class Hono<
|
||||
Object.assign(this, init)
|
||||
}
|
||||
|
||||
private notFoundHandler: NotFoundHandler<E> = (c: Context<string, E>) => {
|
||||
private notFoundHandler: NotFoundHandler<P, E, S> = (c: Context<P, E, S>) => {
|
||||
return c.text('404 Not Found', 404)
|
||||
}
|
||||
|
||||
private errorHandler: ErrorHandler<E> = (err: Error, c: Context<string, E>) => {
|
||||
private errorHandler: ErrorHandler<P, E, S> = (err: Error, c: Context<P, E, S>) => {
|
||||
console.trace(err.message)
|
||||
const message = 'Internal Server Error'
|
||||
return c.text(message, 500)
|
||||
}
|
||||
|
||||
route(path: string, app?: Hono<any>): Hono<E, P, D> {
|
||||
route(path: string, app?: Hono<E, P, S>) {
|
||||
this._tempPath = path
|
||||
if (app) {
|
||||
app.routes.map((r) => {
|
||||
@ -153,18 +166,17 @@ export class Hono<
|
||||
})
|
||||
this._tempPath = ''
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
use<Path extends string = string, Data extends ValidatedData = D>(
|
||||
use<Path extends string = string, Data extends Partial<Schema> = Schema>(
|
||||
...middleware: Handler<Path, E, Data>[]
|
||||
): Hono<E, Path, Data>
|
||||
use<Path extends string = string, Data extends ValidatedData = D>(
|
||||
): Hono<E, Path, S>
|
||||
use<Path extends string = string, Data extends Partial<Schema> = Schema>(
|
||||
arg1: string,
|
||||
...middleware: Handler<Path, E, Data>[]
|
||||
): Hono<E, Path, D>
|
||||
use(arg1: string | Handler<string, E, D>, ...handlers: Handler<string, E, D>[]): Hono<E, P, D> {
|
||||
): Hono<E, Path, S>
|
||||
use(arg1: string | Handler<P, E, S>, ...handlers: Handler<P, E, S>[]) {
|
||||
if (typeof arg1 === 'string') {
|
||||
this.path = arg1
|
||||
} else {
|
||||
@ -176,23 +188,23 @@ export class Hono<
|
||||
return this
|
||||
}
|
||||
|
||||
onError(handler: ErrorHandler<E>): Hono<E, P, D> {
|
||||
onError(handler: ErrorHandler<P, E, S>) {
|
||||
this.errorHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
notFound(handler: NotFoundHandler<E>): Hono<E, P, D> {
|
||||
notFound(handler: NotFoundHandler<P, E, S>) {
|
||||
this.notFoundHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
private addRoute(method: string, path: string, handler: Handler<string, E, D>): void {
|
||||
private addRoute(method: string, path: string, handler: Handler<P, E, S>): void {
|
||||
method = method.toUpperCase()
|
||||
if (this._tempPath) {
|
||||
path = mergePath(this._tempPath, path)
|
||||
}
|
||||
this.router.add(method, path, handler)
|
||||
const r: Route<E, D> = { path: path, method: method, handler: handler }
|
||||
const r: Route<P, E, S> = { path: path, method: method, handler: handler }
|
||||
this.routes.push(r)
|
||||
}
|
||||
|
||||
@ -200,7 +212,7 @@ export class Hono<
|
||||
return this.router.match(method, path)
|
||||
}
|
||||
|
||||
private handleError(err: unknown, c: Context) {
|
||||
private handleError(err: unknown, c: Context<P, E, S>) {
|
||||
if (err instanceof Error) {
|
||||
return this.errorHandler(err, c)
|
||||
}
|
||||
@ -218,12 +230,12 @@ export class Hono<
|
||||
const result = this.matchRoute(method, path)
|
||||
request.paramData = result?.params
|
||||
|
||||
const c = new HonoContext<string, E, D>(request, env, eventOrExecutionCtx, this.notFoundHandler)
|
||||
const c = new HonoContext<P, E, S>(request, env, eventOrExecutionCtx, this.notFoundHandler)
|
||||
|
||||
// Do not `compose` if it has only one handler
|
||||
if (result && result.handlers.length === 1) {
|
||||
const handler = result.handlers[0]
|
||||
let res: ReturnType<Handler>
|
||||
let res: ReturnType<Handler<P>>
|
||||
|
||||
try {
|
||||
res = handler(c, async () => {})
|
||||
@ -249,7 +261,7 @@ export class Hono<
|
||||
}
|
||||
|
||||
const handlers = result ? result.handlers : [this.notFoundHandler]
|
||||
const composed = compose<HonoContext<string, E>, E>(
|
||||
const composed = compose<HonoContext<P, E, S>, P, E, S>(
|
||||
handlers,
|
||||
this.notFoundHandler,
|
||||
this.errorHandler
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Context } from '../../context.ts'
|
||||
import type { Next } from '../../hono.ts'
|
||||
import type { MiddlewareHandler } from '../../hono.ts'
|
||||
import { getContentFromKVAsset } from '../../utils/cloudflare.ts'
|
||||
import { getFilePath } from '../../utils/filepath.ts'
|
||||
import { getMimeType } from '../../utils/mime.ts'
|
||||
@ -14,8 +13,8 @@ export type ServeStaticOptions = {
|
||||
const DEFAULT_DOCUMENT = 'index.html'
|
||||
|
||||
// This middleware is available only on Cloudflare Workers.
|
||||
export const serveStatic = (options: ServeStaticOptions = { root: '' }) => {
|
||||
return async (c: Context, next: Next) => {
|
||||
export const serveStatic = (options: ServeStaticOptions = { root: '' }): MiddlewareHandler => {
|
||||
return async (c, next) => {
|
||||
// Do nothing if Response is already set
|
||||
if (c.finalized) {
|
||||
await next()
|
||||
|
@ -1,62 +1,10 @@
|
||||
import type { Context } from '../../context.ts'
|
||||
import type { Environment, Next, ValidatedData } from '../../hono.ts'
|
||||
import type { Environment, MiddlewareHandler } from '../../hono.ts'
|
||||
import { getStatusText } from '../../utils/http-status.ts'
|
||||
import { mergeObjects } from '../../utils/object.ts'
|
||||
import { VBase, Validator, VObjectBase } from './validator.ts'
|
||||
import type {
|
||||
VString,
|
||||
VNumber,
|
||||
VBoolean,
|
||||
VObject,
|
||||
VNumberArray,
|
||||
VStringArray,
|
||||
VBooleanArray,
|
||||
ValidateResult,
|
||||
VArray,
|
||||
} from './validator.ts'
|
||||
|
||||
export type Schema = {
|
||||
[key: string]:
|
||||
| VString
|
||||
| VNumber
|
||||
| VBoolean
|
||||
| VStringArray
|
||||
| VNumberArray
|
||||
| VBooleanArray
|
||||
| Schema
|
||||
| VObject<Schema>
|
||||
| VArray<Schema>
|
||||
}
|
||||
|
||||
type SchemaToProp<T> = {
|
||||
[K in keyof T]: T[K] extends VNumberArray
|
||||
? number[]
|
||||
: T[K] extends VBooleanArray
|
||||
? boolean[]
|
||||
: T[K] extends VStringArray
|
||||
? string[]
|
||||
: T[K] extends VNumber
|
||||
? number
|
||||
: T[K] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VString
|
||||
? string
|
||||
: T[K] extends VObjectBase<Schema>
|
||||
? T[K]['container'] extends VNumber
|
||||
? number
|
||||
: T[K]['container'] extends VString
|
||||
? string
|
||||
: T[K]['container'] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VArray<Schema>
|
||||
? SchemaToProp<ReadonlyArray<T[K]['container']>>
|
||||
: T[K] extends VObject<Schema>
|
||||
? SchemaToProp<T[K]['container']>
|
||||
: T[K] extends Schema
|
||||
? SchemaToProp<T[K]>
|
||||
: never
|
||||
: SchemaToProp<T[K]>
|
||||
}
|
||||
import type { Schema } from '../../validator/schema.ts'
|
||||
import type { ValidateResult } from '../../validator/validator.ts'
|
||||
import { Validator, VBase, VObjectBase } from '../../validator/validator.ts'
|
||||
|
||||
type ResultSet = {
|
||||
hasError: boolean
|
||||
@ -64,27 +12,27 @@ type ResultSet = {
|
||||
results: ValidateResult[]
|
||||
}
|
||||
|
||||
type Done<Env extends Partial<Environment>> = (
|
||||
type Done<P extends string, E extends Partial<Environment> = Environment> = (
|
||||
resultSet: ResultSet,
|
||||
context: Context<string, Env>
|
||||
c: Context<P, E>
|
||||
) => Response | void
|
||||
|
||||
type ValidationFunction<T, Env extends Partial<Environment>> = (
|
||||
v: Validator,
|
||||
c: Context<string, Env>
|
||||
) => T
|
||||
type ValidationFunction<
|
||||
P extends string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Schema = Schema
|
||||
> = (v: Validator, c: Context<P, E>) => S
|
||||
|
||||
type MiddlewareHandler<
|
||||
Data extends ValidatedData = ValidatedData,
|
||||
Env extends Partial<Environment> = Environment
|
||||
> = (c: Context<string, Env, Data>, next: Next) => Promise<void> | Promise<Response | undefined>
|
||||
|
||||
export const validatorMiddleware = <T extends Schema, Env extends Partial<Environment>>(
|
||||
validationFunction: ValidationFunction<T, Env>,
|
||||
options?: { done?: Done<Env> }
|
||||
export const validatorMiddleware = <
|
||||
P extends string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Schema = Schema
|
||||
>(
|
||||
validationFunction: ValidationFunction<P, E, S>,
|
||||
options?: { done?: Done<P, E> }
|
||||
) => {
|
||||
const v = new Validator()
|
||||
const handler: MiddlewareHandler<SchemaToProp<T>, Env> = async (c, next) => {
|
||||
const handler: MiddlewareHandler<string, E, S> = async (c, next) => {
|
||||
const resultSet: ResultSet = {
|
||||
hasError: false,
|
||||
messages: [],
|
||||
@ -98,7 +46,7 @@ export const validatorMiddleware = <T extends Schema, Env extends Partial<Enviro
|
||||
for (const [keys, validator] of validatorList) {
|
||||
let results: ValidateResult[]
|
||||
try {
|
||||
results = await validator.validate(c.req)
|
||||
results = await validator.validate(c.req as Request)
|
||||
} catch (e) {
|
||||
// Invalid JSON request
|
||||
return c.text(getStatusText(400), 400)
|
||||
|
@ -36,11 +36,12 @@ declare global {
|
||||
}
|
||||
bodyData?: BodyData
|
||||
parseBody<BodyType extends BodyData>(): Promise<BodyType>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
jsonData?: any
|
||||
json<JSONData = any>(): Promise<JSONData>
|
||||
json<JSONData = unknown>(): Promise<Partial<JSONData>>
|
||||
data: Data
|
||||
valid: {
|
||||
(key: string | string[], value: any): Data
|
||||
(key: string | string[], value: unknown): Data
|
||||
(): Data
|
||||
}
|
||||
}
|
||||
@ -132,9 +133,9 @@ export function extendRequestPrototype() {
|
||||
return body
|
||||
} as InstanceType<typeof Request>['parseBody']
|
||||
|
||||
Request.prototype.json = async function <JSONData>(this: Request): Promise<JSONData> {
|
||||
Request.prototype.json = async function <JSONData = unknown>(this: Request) {
|
||||
// Cache the JSON body
|
||||
let jsonData: JSONData
|
||||
let jsonData: Partial<JSONData>
|
||||
if (!this.jsonData) {
|
||||
jsonData = JSON.parse(await this.text())
|
||||
this.jsonData = jsonData
|
||||
@ -144,7 +145,7 @@ export function extendRequestPrototype() {
|
||||
return jsonData
|
||||
} as InstanceType<typeof Request>['jsonData']
|
||||
|
||||
Request.prototype.valid = function (this: Request, keys?: string | string[], value?: any) {
|
||||
Request.prototype.valid = function (this: Request, keys?: string | string[], value?: unknown) {
|
||||
if (!this.data) {
|
||||
this.data = {}
|
||||
}
|
||||
|
5
deno_dist/utils/types.ts
Normal file
5
deno_dist/utils/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type Expect<T extends true> = T
|
||||
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
||||
? true
|
||||
: false
|
||||
export type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true
|
54
deno_dist/validator/schema.ts
Normal file
54
deno_dist/validator/schema.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type {
|
||||
VString,
|
||||
VNumber,
|
||||
VBoolean,
|
||||
VObject,
|
||||
VNumberArray,
|
||||
VStringArray,
|
||||
VBooleanArray,
|
||||
VArray,
|
||||
VObjectBase,
|
||||
} from './validator.ts'
|
||||
|
||||
export type Schema = {
|
||||
[key: string]:
|
||||
| VString
|
||||
| VNumber
|
||||
| VBoolean
|
||||
| VStringArray
|
||||
| VNumberArray
|
||||
| VBooleanArray
|
||||
| Schema
|
||||
| VObject<Schema>
|
||||
| VArray<Schema>
|
||||
}
|
||||
|
||||
export type SchemaToProp<T> = {
|
||||
[K in keyof T]: T[K] extends VNumberArray
|
||||
? number[]
|
||||
: T[K] extends VBooleanArray
|
||||
? boolean[]
|
||||
: T[K] extends VStringArray
|
||||
? string[]
|
||||
: T[K] extends VNumber
|
||||
? number
|
||||
: T[K] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VString
|
||||
? string
|
||||
: T[K] extends VObjectBase<Schema>
|
||||
? T[K]['container'] extends VNumber
|
||||
? number
|
||||
: T[K]['container'] extends VString
|
||||
? string
|
||||
: T[K]['container'] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VArray<Schema>
|
||||
? SchemaToProp<ReadonlyArray<T[K]['container']>>
|
||||
: T[K] extends VObject<Schema>
|
||||
? SchemaToProp<T[K]['container']>
|
||||
: T[K] extends Schema
|
||||
? SchemaToProp<T[K]>
|
||||
: never
|
||||
: SchemaToProp<T[K]>
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { JSONPathCopy } from '../../utils/json.ts'
|
||||
import type { JSONObject, JSONPrimitive, JSONArray } from '../../utils/json.ts'
|
||||
import type { Schema } from './middleware.ts'
|
||||
import { JSONPathCopy } from './../utils/json.ts'
|
||||
import type { JSONObject, JSONPrimitive, JSONArray } from './../utils/json.ts'
|
||||
import { rule } from './rule.ts'
|
||||
import { sanitizer } from './sanitizer.ts'
|
||||
import type { Schema } from './schema.ts'
|
||||
|
||||
type Target = 'query' | 'header' | 'body' | 'json'
|
||||
type Type = JSONPrimitive | JSONObject | JSONArray | File
|
||||
@ -209,7 +209,7 @@ export abstract class VBase {
|
||||
return this
|
||||
}
|
||||
|
||||
validate = async (req: Request): Promise<ValidateResult[]> => {
|
||||
validate = async <R extends Request>(req: R): Promise<ValidateResult[]> => {
|
||||
let value: Type = undefined
|
||||
let jsonData: JSONObject | undefined = undefined
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { HonoContext } from './context'
|
||||
import type { Environment, NotFoundHandler, ErrorHandler } from './hono'
|
||||
import type { Schema } from './validator/schema'
|
||||
|
||||
interface ComposeContext {
|
||||
finalized: boolean
|
||||
res: any
|
||||
res: unknown
|
||||
}
|
||||
|
||||
// Based on the code in the MIT licensed `koa-compose` package.
|
||||
export const compose = <C extends ComposeContext, E extends Partial<Environment> = Environment>(
|
||||
export const compose = <
|
||||
C extends ComposeContext,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends Partial<Schema> = Schema
|
||||
>(
|
||||
middleware: Function[],
|
||||
onNotFound?: NotFoundHandler<E>,
|
||||
onError?: ErrorHandler<E>
|
||||
onNotFound?: NotFoundHandler<P, E, D>,
|
||||
onError?: ErrorHandler<P, E, D>
|
||||
) => {
|
||||
const middlewareLength = middleware.length
|
||||
return (context: C, next?: Function) => {
|
||||
|
@ -1,24 +1,19 @@
|
||||
import type {
|
||||
Environment,
|
||||
NotFoundHandler,
|
||||
ContextVariableMap,
|
||||
Bindings,
|
||||
ValidatedData,
|
||||
} from './hono'
|
||||
import type { Environment, NotFoundHandler, ContextVariableMap, Bindings } from './hono'
|
||||
import type { CookieOptions } from './utils/cookie'
|
||||
import { serialize } from './utils/cookie'
|
||||
import type { StatusCode } from './utils/http-status'
|
||||
import type { Schema, SchemaToProp } from './validator/schema'
|
||||
|
||||
type HeaderField = [string, string]
|
||||
type Headers = Record<string, string | string[]>
|
||||
export type Data = string | ArrayBuffer | ReadableStream
|
||||
|
||||
export interface Context<
|
||||
RequestParamKeyType extends string = string,
|
||||
E extends Partial<Environment> = any,
|
||||
D extends ValidatedData = ValidatedData
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> {
|
||||
req: Request<RequestParamKeyType, D>
|
||||
req: Request<P, SchemaToProp<S>>
|
||||
env: E['Bindings']
|
||||
event: FetchEvent
|
||||
executionCtx: ExecutionContext
|
||||
@ -32,7 +27,7 @@ export interface Context<
|
||||
set: {
|
||||
<Key extends keyof ContextVariableMap>(key: Key, value: ContextVariableMap[Key]): void
|
||||
<Key extends keyof E['Variables']>(key: Key, value: E['Variables'][Key]): void
|
||||
(key: string, value: any): void
|
||||
(key: string, value: unknown): void
|
||||
}
|
||||
get: {
|
||||
<Key extends keyof ContextVariableMap>(key: Key): ContextVariableMap[Key]
|
||||
@ -51,12 +46,12 @@ export interface Context<
|
||||
}
|
||||
|
||||
export class HonoContext<
|
||||
RequestParamKeyType extends string = string,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
> implements Context<RequestParamKeyType, E, D>
|
||||
S extends Partial<Schema> = Schema
|
||||
> implements Context<P, E, S>
|
||||
{
|
||||
req: Request<RequestParamKeyType, D>
|
||||
req: Request<P, SchemaToProp<S>>
|
||||
env: E['Bindings']
|
||||
finalized: boolean
|
||||
error: Error | undefined = undefined
|
||||
@ -65,19 +60,19 @@ export class HonoContext<
|
||||
private _executionCtx: FetchEvent | ExecutionContext | undefined
|
||||
private _pretty: boolean = false
|
||||
private _prettySpace: number = 2
|
||||
private _map: Record<string, any> | undefined
|
||||
private _map: Record<string, unknown> | undefined
|
||||
private _headers: Record<string, string[]> | undefined
|
||||
private _res: Response | undefined
|
||||
private notFoundHandler: NotFoundHandler<E>
|
||||
private notFoundHandler: NotFoundHandler<P, E, S>
|
||||
|
||||
constructor(
|
||||
req: Request<RequestParamKeyType>,
|
||||
env: E['Bindings'] | undefined = undefined,
|
||||
req: Request<P>,
|
||||
env: E['Bindings'] = {},
|
||||
executionCtx: FetchEvent | ExecutionContext | undefined = undefined,
|
||||
notFoundHandler: NotFoundHandler<E> = () => new Response()
|
||||
notFoundHandler: NotFoundHandler<P, E, S> = () => new Response()
|
||||
) {
|
||||
this._executionCtx = executionCtx
|
||||
this.req = req as Request<RequestParamKeyType, D>
|
||||
this.req = req as Request<P, SchemaToProp<S>>
|
||||
this.env = env || ({} as Bindings)
|
||||
|
||||
this.notFoundHandler = notFoundHandler
|
||||
@ -142,15 +137,15 @@ export class HonoContext<
|
||||
|
||||
set<Key extends keyof ContextVariableMap>(key: Key, value: ContextVariableMap[Key]): void
|
||||
set<Key extends keyof E['Variables']>(key: Key, value: E['Variables'][Key]): void
|
||||
set(key: string, value: any): void
|
||||
set(key: string, value: any): void {
|
||||
set(key: string, value: unknown): void
|
||||
set(key: string, value: unknown): void {
|
||||
this._map ||= {}
|
||||
this._map[key] = value
|
||||
}
|
||||
|
||||
get<Key extends keyof ContextVariableMap>(key: Key): ContextVariableMap[Key]
|
||||
get<Key extends keyof E['Variables']>(key: Key): E['Variables'][Key]
|
||||
get<T = any>(key: string): T
|
||||
get<T>(key: string): T
|
||||
get(key: string) {
|
||||
if (!this._map) {
|
||||
return undefined
|
||||
@ -233,6 +228,6 @@ export class HonoContext<
|
||||
}
|
||||
|
||||
notFound(): Response | Promise<Response> {
|
||||
return this.notFoundHandler(this as any)
|
||||
return this.notFoundHandler(this)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type { Context } from './context'
|
||||
import type { Next } from './hono'
|
||||
import type { Handler, Next } from './hono'
|
||||
import { Hono } from './hono'
|
||||
import { logger } from './middleware/logger'
|
||||
import { poweredBy } from './middleware/powered-by'
|
||||
import type { Expect, Equal } from './utils/types'
|
||||
|
||||
describe('GET Request', () => {
|
||||
const app = new Hono()
|
||||
@ -1137,3 +1139,65 @@ describe('Count of logger called', () => {
|
||||
expect(log).toMatch(/404/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context set/get variables', () => {
|
||||
type Variables = {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
const app = new Hono<{ Variables: Variables }>()
|
||||
|
||||
it('Should set and get variables with correct types', async () => {
|
||||
app.use('*', async (c, next) => {
|
||||
c.set('id', 123)
|
||||
c.set('title', 'Hello')
|
||||
await next()
|
||||
})
|
||||
app.get('/', (c) => {
|
||||
const id = c.get('id')
|
||||
const title = c.get('title')
|
||||
type verifyID = Expect<Equal<typeof id, number>>
|
||||
type verifyTitle = Expect<Equal<typeof title, string>>
|
||||
return c.text(`${id} is ${title}`)
|
||||
})
|
||||
const res = await app.request('http://localhost/')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('123 is Hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context binding variables', () => {
|
||||
type Bindings = {
|
||||
USER_ID: number
|
||||
USER_NAME: string
|
||||
}
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>()
|
||||
|
||||
it('Should get binding variables with correct types', async () => {
|
||||
app.get('/', (c) => {
|
||||
type verifyID = Expect<Equal<typeof c.env.USER_ID, number>>
|
||||
type verifyName = Expect<Equal<typeof c.env.USER_NAME, string>>
|
||||
return c.text('These are verified')
|
||||
})
|
||||
const res = await app.request('http://localhost/')
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Handler as variables', () => {
|
||||
const app = new Hono()
|
||||
|
||||
it('Should be typed correctly', async () => {
|
||||
const handler: Handler = (c) => {
|
||||
const id = c.req.param('id')
|
||||
return c.text(`Post id is ${id}`)
|
||||
}
|
||||
app.get('/posts/:id', handler)
|
||||
|
||||
const res = await app.request('http://localhost/posts/123')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('Post id is 123')
|
||||
})
|
||||
})
|
||||
|
122
src/hono.ts
122
src/hono.ts
@ -10,6 +10,7 @@ import { SmartRouter } from './router/smart-router'
|
||||
import { StaticRouter } from './router/static-router'
|
||||
import { TrieRouter } from './router/trie-router'
|
||||
import { getPathFromURL, mergePath } from './utils/url'
|
||||
import type { Schema } from './validator/schema'
|
||||
|
||||
export interface ContextVariableMap {}
|
||||
|
||||
@ -20,30 +21,29 @@ export type Environment = {
|
||||
Variables: Variables
|
||||
}
|
||||
|
||||
export type ValidatedData = Record<string, any> // For validated data
|
||||
|
||||
export type Handler<
|
||||
RequestParamKeyType extends string = string,
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
> = (
|
||||
c: Context<RequestParamKeyType, E, D>,
|
||||
next: Next
|
||||
) => Response | Promise<Response> | Promise<void> | Promise<Response | undefined | void>
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>, next: Next) => Response | Promise<Response | undefined | void>
|
||||
|
||||
export type MiddlewareHandler = <E extends Partial<Environment> = Environment>(
|
||||
c: Context<string, E>,
|
||||
next: Next
|
||||
) => Promise<void> | Promise<Response | undefined>
|
||||
export type MiddlewareHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>, next: Next) => Promise<Response | undefined | void>
|
||||
|
||||
export type NotFoundHandler<E extends Partial<Environment> = Environment> = (
|
||||
c: Context<string, E>
|
||||
) => Response | Promise<Response>
|
||||
export type NotFoundHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (c: Context<P, E, S>) => Response | Promise<Response>
|
||||
|
||||
export type ErrorHandler<E extends Partial<Environment> = Environment> = (
|
||||
err: Error,
|
||||
c: Context<string, E>
|
||||
) => Response
|
||||
export type ErrorHandler<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Partial<Schema> = Schema
|
||||
> = (err: Error, c: Context<P, E, S>) => Response
|
||||
|
||||
export type Next = () => Promise<void>
|
||||
|
||||
@ -60,52 +60,65 @@ type ParamKeys<Path> = Path extends `${infer Component}/${infer Rest}`
|
||||
? ParamKey<Component> | ParamKeys<Rest>
|
||||
: ParamKey<Path>
|
||||
|
||||
interface HandlerInterface<T extends string, E extends Partial<Environment>, U = Hono<E, T>> {
|
||||
interface HandlerInterface<
|
||||
P extends string,
|
||||
E extends Partial<Environment>,
|
||||
S extends Partial<Schema>,
|
||||
U = Hono<E, P, S>
|
||||
> {
|
||||
// app.get(handler...)
|
||||
<Path extends string, Data extends ValidatedData>(
|
||||
<Path extends string, Data extends Schema>(
|
||||
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E, Data>[]
|
||||
): U
|
||||
(...handlers: Handler<string, E>[]): U
|
||||
(...handlers: Handler<string, E, S>[]): U
|
||||
|
||||
// app.get('/', handler, handler...)
|
||||
<Path extends string, Data extends ValidatedData>(
|
||||
<Path extends string, Data extends Partial<Schema> = Schema>(
|
||||
path: Path,
|
||||
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E, Data>[]
|
||||
): U
|
||||
(path: string, ...handlers: Handler<string, E>[]): U
|
||||
<Path extends string, Data extends Schema>(path: Path, ...handlers: Handler<string, E, Data>[]): U
|
||||
(path: string, ...handlers: Handler<string, E, S>[]): U
|
||||
}
|
||||
|
||||
type Methods = typeof METHODS[number] | typeof METHOD_NAME_ALL_LOWERCASE
|
||||
|
||||
function defineDynamicClass(): {
|
||||
new <E extends Partial<Environment> = Environment, T extends string = string, U = Hono<E, T>>(): {
|
||||
[K in Methods]: HandlerInterface<T, E, U>
|
||||
new <
|
||||
E extends Partial<Environment> = Environment,
|
||||
P extends string = string,
|
||||
S extends Partial<Schema> = Schema,
|
||||
U = Hono<E, P, S>
|
||||
>(): {
|
||||
[K in Methods]: HandlerInterface<P, E, S, U>
|
||||
}
|
||||
} {
|
||||
return class {} as any
|
||||
return class {} as never
|
||||
}
|
||||
|
||||
interface Route<
|
||||
P extends string = string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
D extends ValidatedData = ValidatedData
|
||||
S extends Partial<Schema> = Schema
|
||||
> {
|
||||
path: string
|
||||
method: string
|
||||
handler: Handler<string, E, D>
|
||||
handler: Handler<P, E, S>
|
||||
}
|
||||
|
||||
export class Hono<
|
||||
E extends Partial<Environment> = Environment,
|
||||
P extends string = '/',
|
||||
D extends ValidatedData = ValidatedData
|
||||
> extends defineDynamicClass()<E, P, Hono<E, P, D>> {
|
||||
readonly router: Router<Handler<string, E, D>> = new SmartRouter({
|
||||
S extends Partial<Schema> = Schema
|
||||
> extends defineDynamicClass()<E, P, S, Hono<E, P, S>> {
|
||||
readonly router: Router<Handler<P, E, S>> = new SmartRouter({
|
||||
routers: [new StaticRouter(), new RegExpRouter(), new TrieRouter()],
|
||||
})
|
||||
readonly strict: boolean = true // strict routing - default is true
|
||||
private _tempPath: string = ''
|
||||
private path: string = '/'
|
||||
|
||||
routes: Route<E, D>[] = []
|
||||
routes: Route<P, E, S>[] = []
|
||||
|
||||
constructor(init: Partial<Pick<Hono, 'router' | 'strict'>> = {}) {
|
||||
super()
|
||||
@ -114,18 +127,18 @@ export class Hono<
|
||||
|
||||
const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE]
|
||||
allMethods.map((method) => {
|
||||
this[method] = <Path extends string = ''>(
|
||||
args1: Path | Handler<ParamKeys<Path>, E, D>,
|
||||
...args: [Handler<ParamKeys<Path>, E, D>]
|
||||
this[method] = <Path extends string, Env extends Environment, Data extends Schema>(
|
||||
args1: Path | Handler<ParamKeys<Path>, Env, Data>,
|
||||
...args: [Handler<ParamKeys<Path>, Env, Data>]
|
||||
): this => {
|
||||
if (typeof args1 === 'string') {
|
||||
this.path = args1
|
||||
} else {
|
||||
this.addRoute(method, this.path, args1)
|
||||
this.addRoute(method, this.path, args1 as unknown as Handler<P, E, S>)
|
||||
}
|
||||
args.map((handler) => {
|
||||
if (typeof handler !== 'string') {
|
||||
this.addRoute(method, this.path, handler)
|
||||
this.addRoute(method, this.path, handler as unknown as Handler<P, E, S>)
|
||||
}
|
||||
})
|
||||
return this
|
||||
@ -135,17 +148,17 @@ export class Hono<
|
||||
Object.assign(this, init)
|
||||
}
|
||||
|
||||
private notFoundHandler: NotFoundHandler<E> = (c: Context<string, E>) => {
|
||||
private notFoundHandler: NotFoundHandler<P, E, S> = (c: Context<P, E, S>) => {
|
||||
return c.text('404 Not Found', 404)
|
||||
}
|
||||
|
||||
private errorHandler: ErrorHandler<E> = (err: Error, c: Context<string, E>) => {
|
||||
private errorHandler: ErrorHandler<P, E, S> = (err: Error, c: Context<P, E, S>) => {
|
||||
console.trace(err.message)
|
||||
const message = 'Internal Server Error'
|
||||
return c.text(message, 500)
|
||||
}
|
||||
|
||||
route(path: string, app?: Hono<any>): Hono<E, P, D> {
|
||||
route(path: string, app?: Hono<E, P, S>) {
|
||||
this._tempPath = path
|
||||
if (app) {
|
||||
app.routes.map((r) => {
|
||||
@ -153,18 +166,17 @@ export class Hono<
|
||||
})
|
||||
this._tempPath = ''
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
use<Path extends string = string, Data extends ValidatedData = D>(
|
||||
use<Path extends string = string, Data extends Partial<Schema> = Schema>(
|
||||
...middleware: Handler<Path, E, Data>[]
|
||||
): Hono<E, Path, Data>
|
||||
use<Path extends string = string, Data extends ValidatedData = D>(
|
||||
): Hono<E, Path, S>
|
||||
use<Path extends string = string, Data extends Partial<Schema> = Schema>(
|
||||
arg1: string,
|
||||
...middleware: Handler<Path, E, Data>[]
|
||||
): Hono<E, Path, D>
|
||||
use(arg1: string | Handler<string, E, D>, ...handlers: Handler<string, E, D>[]): Hono<E, P, D> {
|
||||
): Hono<E, Path, S>
|
||||
use(arg1: string | Handler<P, E, S>, ...handlers: Handler<P, E, S>[]) {
|
||||
if (typeof arg1 === 'string') {
|
||||
this.path = arg1
|
||||
} else {
|
||||
@ -176,23 +188,23 @@ export class Hono<
|
||||
return this
|
||||
}
|
||||
|
||||
onError(handler: ErrorHandler<E>): Hono<E, P, D> {
|
||||
onError(handler: ErrorHandler<P, E, S>) {
|
||||
this.errorHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
notFound(handler: NotFoundHandler<E>): Hono<E, P, D> {
|
||||
notFound(handler: NotFoundHandler<P, E, S>) {
|
||||
this.notFoundHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
private addRoute(method: string, path: string, handler: Handler<string, E, D>): void {
|
||||
private addRoute(method: string, path: string, handler: Handler<P, E, S>): void {
|
||||
method = method.toUpperCase()
|
||||
if (this._tempPath) {
|
||||
path = mergePath(this._tempPath, path)
|
||||
}
|
||||
this.router.add(method, path, handler)
|
||||
const r: Route<E, D> = { path: path, method: method, handler: handler }
|
||||
const r: Route<P, E, S> = { path: path, method: method, handler: handler }
|
||||
this.routes.push(r)
|
||||
}
|
||||
|
||||
@ -200,7 +212,7 @@ export class Hono<
|
||||
return this.router.match(method, path)
|
||||
}
|
||||
|
||||
private handleError(err: unknown, c: Context) {
|
||||
private handleError(err: unknown, c: Context<P, E, S>) {
|
||||
if (err instanceof Error) {
|
||||
return this.errorHandler(err, c)
|
||||
}
|
||||
@ -218,12 +230,12 @@ export class Hono<
|
||||
const result = this.matchRoute(method, path)
|
||||
request.paramData = result?.params
|
||||
|
||||
const c = new HonoContext<string, E, D>(request, env, eventOrExecutionCtx, this.notFoundHandler)
|
||||
const c = new HonoContext<P, E, S>(request, env, eventOrExecutionCtx, this.notFoundHandler)
|
||||
|
||||
// Do not `compose` if it has only one handler
|
||||
if (result && result.handlers.length === 1) {
|
||||
const handler = result.handlers[0]
|
||||
let res: ReturnType<Handler>
|
||||
let res: ReturnType<Handler<P>>
|
||||
|
||||
try {
|
||||
res = handler(c, async () => {})
|
||||
@ -249,7 +261,7 @@ export class Hono<
|
||||
}
|
||||
|
||||
const handlers = result ? result.handlers : [this.notFoundHandler]
|
||||
const composed = compose<HonoContext<string, E>, E>(
|
||||
const composed = compose<HonoContext<P, E, S>, P, E, S>(
|
||||
handlers,
|
||||
this.notFoundHandler,
|
||||
this.errorHandler
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Context } from '../../context'
|
||||
import type { Next } from '../../hono'
|
||||
import type { MiddlewareHandler } from '../../hono'
|
||||
import { getContentFromKVAsset } from '../../utils/cloudflare'
|
||||
import { getFilePath } from '../../utils/filepath'
|
||||
import { getMimeType } from '../../utils/mime'
|
||||
@ -14,8 +13,8 @@ export type ServeStaticOptions = {
|
||||
const DEFAULT_DOCUMENT = 'index.html'
|
||||
|
||||
// This middleware is available only on Cloudflare Workers.
|
||||
export const serveStatic = (options: ServeStaticOptions = { root: '' }) => {
|
||||
return async (c: Context, next: Next) => {
|
||||
export const serveStatic = (options: ServeStaticOptions = { root: '' }): MiddlewareHandler => {
|
||||
return async (c, next) => {
|
||||
// Do nothing if Response is already set
|
||||
if (c.finalized) {
|
||||
await next()
|
||||
|
@ -1,5 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Hono } from '../../hono'
|
||||
import { getStatusText } from '../../utils/http-status'
|
||||
import type { Expect, Equal } from '../../utils/types'
|
||||
import type { Schema } from '../../validator/schema'
|
||||
import { validator } from './index'
|
||||
|
||||
describe('Basic - query', () => {
|
||||
@ -102,15 +105,19 @@ describe('Basic - header & custom message', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic - JSON', () => {
|
||||
describe('Basic - JSON with type check', () => {
|
||||
const app = new Hono()
|
||||
// JSON
|
||||
app.post(
|
||||
'/json',
|
||||
validator((v) => ({
|
||||
id: v.json('post.author.id').asNumber(),
|
||||
name: v.json('post.author.name').isAlpha(),
|
||||
})),
|
||||
(c) => {
|
||||
const { id, name } = c.req.valid()
|
||||
type verifyID = Expect<Equal<typeof id, number>>
|
||||
type verifyName = Expect<Equal<typeof name, string>>
|
||||
return c.text('Valid')
|
||||
}
|
||||
)
|
||||
@ -119,6 +126,7 @@ describe('Basic - JSON', () => {
|
||||
const json = {
|
||||
post: {
|
||||
author: {
|
||||
id: 123,
|
||||
name: 'abcdef',
|
||||
},
|
||||
},
|
||||
@ -579,7 +587,7 @@ describe('Structured data', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Array values', () => {
|
||||
describe('Array values with type check', () => {
|
||||
const app = new Hono()
|
||||
app.post(
|
||||
'/post',
|
||||
@ -591,8 +599,13 @@ describe('Array values', () => {
|
||||
},
|
||||
})),
|
||||
(c) => {
|
||||
const res = c.req.valid()
|
||||
return c.json({ tag1: res.post.tags[0] })
|
||||
const { post } = c.req.valid()
|
||||
|
||||
type verifyTitle = Expect<Equal<typeof post.title, string>>
|
||||
type verifyTags = Expect<Equal<typeof post.tags, string[]>>
|
||||
type verifyIDs = Expect<Equal<typeof post.ids, number[]>>
|
||||
|
||||
return c.json({ tag1: post.tags[0] })
|
||||
}
|
||||
)
|
||||
|
||||
@ -730,3 +743,98 @@ describe('Special case', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type check in special case', () => {
|
||||
it('Should return 200 response with correct types', async () => {
|
||||
const app = new Hono()
|
||||
app.post(
|
||||
'/posts/:id',
|
||||
validator((v) => ({
|
||||
title: v.body('title').isRequired(),
|
||||
})),
|
||||
(c) => {
|
||||
const res = c.req.valid()
|
||||
const id = c.req.param('id')
|
||||
type verifyTitle = Expect<Equal<typeof res.title, string>>
|
||||
type verifyId = Expect<Equal<typeof id, string>>
|
||||
return c.text(`${id} is ${res.title}`)
|
||||
}
|
||||
)
|
||||
const body = new FormData()
|
||||
body.append('title', 'Hello')
|
||||
const req = new Request('http://localhost/posts/123', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
const res = await app.request(req)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('123 is Hello')
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct types - validator and named parameter', async () => {
|
||||
const app = new Hono()
|
||||
|
||||
const vm = validator((v) => ({
|
||||
title: v.body('title').isRequired(),
|
||||
}))
|
||||
|
||||
app.post('/posts', vm, (c) => {
|
||||
const { title } = c.req.valid()
|
||||
type verify = Expect<Equal<typeof title, string>>
|
||||
return c.text(title)
|
||||
})
|
||||
|
||||
app.post('/posts/:id', vm, (c) => {
|
||||
const id = c.req.param('id')
|
||||
const { title } = c.req.valid()
|
||||
type verify = Expect<Equal<typeof title, string>>
|
||||
return c.text(`${id} is ${title}`)
|
||||
})
|
||||
|
||||
const body = new FormData()
|
||||
body.append('title', 'Hello')
|
||||
|
||||
let res = await app.request('http://localhost/posts', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('Hello')
|
||||
res = await app.request('http://localhost/posts/123?title=foo', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('123 is Hello')
|
||||
})
|
||||
|
||||
it('Should return 200 response with correct types - Context in validator function', async () => {
|
||||
type Env = { Bindings: { FOO: 'abc' } }
|
||||
|
||||
const app = new Hono<Env>()
|
||||
|
||||
app.get(
|
||||
'/search',
|
||||
validator(
|
||||
(v) => ({
|
||||
foo: v.query('foo'),
|
||||
}),
|
||||
{
|
||||
done: (_, c) => {
|
||||
type verifyBindings = Expect<Equal<typeof c.env.FOO, 'abc'>>
|
||||
},
|
||||
}
|
||||
),
|
||||
(c) => {
|
||||
const { foo } = c.req.valid()
|
||||
type verifyBindings = Expect<Equal<typeof c.env.FOO, 'abc'>>
|
||||
type verify = Expect<Equal<typeof foo, string>>
|
||||
return c.text(foo)
|
||||
}
|
||||
)
|
||||
|
||||
const res = await app.request('http://localhost/search?foo=bar')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('bar')
|
||||
})
|
||||
})
|
||||
|
@ -1,62 +1,10 @@
|
||||
import type { Context } from '../../context'
|
||||
import type { Environment, Next, ValidatedData } from '../../hono'
|
||||
import type { Environment, MiddlewareHandler } from '../../hono'
|
||||
import { getStatusText } from '../../utils/http-status'
|
||||
import { mergeObjects } from '../../utils/object'
|
||||
import { VBase, Validator, VObjectBase } from './validator'
|
||||
import type {
|
||||
VString,
|
||||
VNumber,
|
||||
VBoolean,
|
||||
VObject,
|
||||
VNumberArray,
|
||||
VStringArray,
|
||||
VBooleanArray,
|
||||
ValidateResult,
|
||||
VArray,
|
||||
} from './validator'
|
||||
|
||||
export type Schema = {
|
||||
[key: string]:
|
||||
| VString
|
||||
| VNumber
|
||||
| VBoolean
|
||||
| VStringArray
|
||||
| VNumberArray
|
||||
| VBooleanArray
|
||||
| Schema
|
||||
| VObject<Schema>
|
||||
| VArray<Schema>
|
||||
}
|
||||
|
||||
type SchemaToProp<T> = {
|
||||
[K in keyof T]: T[K] extends VNumberArray
|
||||
? number[]
|
||||
: T[K] extends VBooleanArray
|
||||
? boolean[]
|
||||
: T[K] extends VStringArray
|
||||
? string[]
|
||||
: T[K] extends VNumber
|
||||
? number
|
||||
: T[K] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VString
|
||||
? string
|
||||
: T[K] extends VObjectBase<Schema>
|
||||
? T[K]['container'] extends VNumber
|
||||
? number
|
||||
: T[K]['container'] extends VString
|
||||
? string
|
||||
: T[K]['container'] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VArray<Schema>
|
||||
? SchemaToProp<ReadonlyArray<T[K]['container']>>
|
||||
: T[K] extends VObject<Schema>
|
||||
? SchemaToProp<T[K]['container']>
|
||||
: T[K] extends Schema
|
||||
? SchemaToProp<T[K]>
|
||||
: never
|
||||
: SchemaToProp<T[K]>
|
||||
}
|
||||
import type { Schema } from '../../validator/schema'
|
||||
import type { ValidateResult } from '../../validator/validator'
|
||||
import { Validator, VBase, VObjectBase } from '../../validator/validator'
|
||||
|
||||
type ResultSet = {
|
||||
hasError: boolean
|
||||
@ -64,27 +12,27 @@ type ResultSet = {
|
||||
results: ValidateResult[]
|
||||
}
|
||||
|
||||
type Done<Env extends Partial<Environment>> = (
|
||||
type Done<P extends string, E extends Partial<Environment> = Environment> = (
|
||||
resultSet: ResultSet,
|
||||
context: Context<string, Env>
|
||||
c: Context<P, E>
|
||||
) => Response | void
|
||||
|
||||
type ValidationFunction<T, Env extends Partial<Environment>> = (
|
||||
v: Validator,
|
||||
c: Context<string, Env>
|
||||
) => T
|
||||
type ValidationFunction<
|
||||
P extends string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Schema = Schema
|
||||
> = (v: Validator, c: Context<P, E>) => S
|
||||
|
||||
type MiddlewareHandler<
|
||||
Data extends ValidatedData = ValidatedData,
|
||||
Env extends Partial<Environment> = Environment
|
||||
> = (c: Context<string, Env, Data>, next: Next) => Promise<void> | Promise<Response | undefined>
|
||||
|
||||
export const validatorMiddleware = <T extends Schema, Env extends Partial<Environment>>(
|
||||
validationFunction: ValidationFunction<T, Env>,
|
||||
options?: { done?: Done<Env> }
|
||||
export const validatorMiddleware = <
|
||||
P extends string,
|
||||
E extends Partial<Environment> = Environment,
|
||||
S extends Schema = Schema
|
||||
>(
|
||||
validationFunction: ValidationFunction<P, E, S>,
|
||||
options?: { done?: Done<P, E> }
|
||||
) => {
|
||||
const v = new Validator()
|
||||
const handler: MiddlewareHandler<SchemaToProp<T>, Env> = async (c, next) => {
|
||||
const handler: MiddlewareHandler<string, E, S> = async (c, next) => {
|
||||
const resultSet: ResultSet = {
|
||||
hasError: false,
|
||||
messages: [],
|
||||
@ -98,7 +46,7 @@ export const validatorMiddleware = <T extends Schema, Env extends Partial<Enviro
|
||||
for (const [keys, validator] of validatorList) {
|
||||
let results: ValidateResult[]
|
||||
try {
|
||||
results = await validator.validate(c.req)
|
||||
results = await validator.validate(c.req as Request)
|
||||
} catch (e) {
|
||||
// Invalid JSON request
|
||||
return c.text(getStatusText(400), 400)
|
||||
|
@ -36,11 +36,12 @@ declare global {
|
||||
}
|
||||
bodyData?: BodyData
|
||||
parseBody<BodyType extends BodyData>(): Promise<BodyType>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
jsonData?: any
|
||||
json<JSONData = any>(): Promise<JSONData>
|
||||
json<JSONData = unknown>(): Promise<Partial<JSONData>>
|
||||
data: Data
|
||||
valid: {
|
||||
(key: string | string[], value: any): Data
|
||||
(key: string | string[], value: unknown): Data
|
||||
(): Data
|
||||
}
|
||||
}
|
||||
@ -132,9 +133,9 @@ export function extendRequestPrototype() {
|
||||
return body
|
||||
} as InstanceType<typeof Request>['parseBody']
|
||||
|
||||
Request.prototype.json = async function <JSONData>(this: Request): Promise<JSONData> {
|
||||
Request.prototype.json = async function <JSONData = unknown>(this: Request) {
|
||||
// Cache the JSON body
|
||||
let jsonData: JSONData
|
||||
let jsonData: Partial<JSONData>
|
||||
if (!this.jsonData) {
|
||||
jsonData = JSON.parse(await this.text())
|
||||
this.jsonData = jsonData
|
||||
@ -144,7 +145,7 @@ export function extendRequestPrototype() {
|
||||
return jsonData
|
||||
} as InstanceType<typeof Request>['jsonData']
|
||||
|
||||
Request.prototype.valid = function (this: Request, keys?: string | string[], value?: any) {
|
||||
Request.prototype.valid = function (this: Request, keys?: string | string[], value?: unknown) {
|
||||
if (!this.data) {
|
||||
this.data = {}
|
||||
}
|
||||
|
5
src/utils/types.ts
Normal file
5
src/utils/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type Expect<T extends true> = T
|
||||
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
||||
? true
|
||||
: false
|
||||
export type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true
|
54
src/validator/schema.ts
Normal file
54
src/validator/schema.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type {
|
||||
VString,
|
||||
VNumber,
|
||||
VBoolean,
|
||||
VObject,
|
||||
VNumberArray,
|
||||
VStringArray,
|
||||
VBooleanArray,
|
||||
VArray,
|
||||
VObjectBase,
|
||||
} from './validator'
|
||||
|
||||
export type Schema = {
|
||||
[key: string]:
|
||||
| VString
|
||||
| VNumber
|
||||
| VBoolean
|
||||
| VStringArray
|
||||
| VNumberArray
|
||||
| VBooleanArray
|
||||
| Schema
|
||||
| VObject<Schema>
|
||||
| VArray<Schema>
|
||||
}
|
||||
|
||||
export type SchemaToProp<T> = {
|
||||
[K in keyof T]: T[K] extends VNumberArray
|
||||
? number[]
|
||||
: T[K] extends VBooleanArray
|
||||
? boolean[]
|
||||
: T[K] extends VStringArray
|
||||
? string[]
|
||||
: T[K] extends VNumber
|
||||
? number
|
||||
: T[K] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VString
|
||||
? string
|
||||
: T[K] extends VObjectBase<Schema>
|
||||
? T[K]['container'] extends VNumber
|
||||
? number
|
||||
: T[K]['container'] extends VString
|
||||
? string
|
||||
: T[K]['container'] extends VBoolean
|
||||
? boolean
|
||||
: T[K] extends VArray<Schema>
|
||||
? SchemaToProp<ReadonlyArray<T[K]['container']>>
|
||||
: T[K] extends VObject<Schema>
|
||||
? SchemaToProp<T[K]['container']>
|
||||
: T[K] extends Schema
|
||||
? SchemaToProp<T[K]>
|
||||
: never
|
||||
: SchemaToProp<T[K]>
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { extendRequestPrototype } from '../../request'
|
||||
import { extendRequestPrototype } from '../request'
|
||||
import { Validator } from './validator'
|
||||
|
||||
extendRequestPrototype()
|
@ -1,8 +1,8 @@
|
||||
import { JSONPathCopy } from '../../utils/json'
|
||||
import type { JSONObject, JSONPrimitive, JSONArray } from '../../utils/json'
|
||||
import type { Schema } from './middleware'
|
||||
import { JSONPathCopy } from './../utils/json'
|
||||
import type { JSONObject, JSONPrimitive, JSONArray } from './../utils/json'
|
||||
import { rule } from './rule'
|
||||
import { sanitizer } from './sanitizer'
|
||||
import type { Schema } from './schema'
|
||||
|
||||
type Target = 'query' | 'header' | 'body' | 'json'
|
||||
type Type = JSONPrimitive | JSONObject | JSONArray | File
|
||||
@ -209,7 +209,7 @@ export abstract class VBase {
|
||||
return this
|
||||
}
|
||||
|
||||
validate = async (req: Request): Promise<ValidateResult[]> => {
|
||||
validate = async <R extends Request>(req: R): Promise<ValidateResult[]> => {
|
||||
let value: Type = undefined
|
||||
let jsonData: JSONObject | undefined = undefined
|
||||
|
@ -5,6 +5,8 @@
|
||||
"declaration": false,
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./dist/cjs/",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
|
@ -4,6 +4,8 @@
|
||||
"module": "ES2020",
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./dist/",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
@ -10,8 +10,8 @@
|
||||
"skipLibCheck": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
|
Loading…
Reference in New Issue
Block a user