0
0
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:
Yusuke Wada 2022-10-23 08:10:00 +09:00 committed by GitHub
parent 77b0815d22
commit 8627010094
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 564 additions and 348 deletions

View File

@ -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) => {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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
View 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

View 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]>
}

View File

@ -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

View File

@ -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) => {

View File

@ -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)
}
}

View File

@ -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')
})
})

View File

@ -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

View File

@ -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()

View File

@ -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')
})
})

View File

@ -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)

View File

@ -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
View 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
View 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]>
}

View File

@ -1,4 +1,4 @@
import { extendRequestPrototype } from '../../request'
import { extendRequestPrototype } from '../request'
import { Validator } from './validator'
extendRequestPrototype()

View File

@ -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

View File

@ -5,6 +5,8 @@
"declaration": false,
"rootDir": "./src/",
"outDir": "./dist/cjs/",
"noUnusedLocals": true,
"noUnusedParameters": true,
},
"include": [
"src/**/*.ts"

View File

@ -4,6 +4,8 @@
"module": "ES2020",
"rootDir": "./src/",
"outDir": "./dist/",
"noUnusedLocals": true,
"noUnusedParameters": true,
},
"include": [
"src/**/*.ts",

View File

@ -10,8 +10,8 @@
"skipLibCheck": true,
"strictPropertyInitialization": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"types": [
"jest",
"node",