Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(validator): Introduce responseValidator #3843

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,4 +862,11 @@
this.#notFoundHandler ??= () => new Response()
return this.#notFoundHandler(this)
}

validateData: unknown

validate = <T>(data: T): T => {
this.validateData = data

Check warning on line 869 in src/context.ts

View check run for this annotation

Codecov / codecov/patch

src/context.ts#L869

Added line #L869 was not covered by tests
return data
}

Check warning on line 871 in src/context.ts

View check run for this annotation

Codecov / codecov/patch

src/context.ts#L871

Added line #L871 was not covered by tests
}
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,26 @@ export type ValidationTargets<T extends FormValue = ParsedFormValue, P extends s
cookie: Record<string, string>
}

////////////////////////////////////////////////
////// /////
////// ResponseValidationTargets /////
////// /////
////////////////////////////////////////////////

export type ResponseValidationTargets = {
body: ReadableStream<any> | null
text: string
json: any
html: string
header: Record<RequestHeader | CustomHeader, string>
cookie: Record<string, string>
status: {
ok: boolean
status: StatusCode
statusText: string
}
}

////////////////////////////////////////
////// //////
////// Path parameters //////
Expand Down
3 changes: 3 additions & 0 deletions src/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

export { validator } from './validator'
export type { ValidationFunction } from './validator'

export { responseValidator } from './response-validator'
export type { ResponseValidationFunction } from './response-validator'
30 changes: 30 additions & 0 deletions src/validator/response-validator.sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Hono } from '../preset/quick'
import { validator, responseValidator } from '.'

Check warning on line 2 in src/validator/response-validator.sandbox.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.sandbox.ts#L1-L2

Added lines #L1 - L2 were not covered by tests

const app = new Hono<{

Check warning on line 4 in src/validator/response-validator.sandbox.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.sandbox.ts#L4

Added line #L4 was not covered by tests
Bindings: {
foo: string
}
}>()

Check warning on line 8 in src/validator/response-validator.sandbox.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.sandbox.ts#L8

Added line #L8 was not covered by tests

app
.get(
'*',
validator('query', (value, c) => {
console.log(value, c)
}),
responseValidator('text', (value, c) => {
console.log(value)
if (!/\d/.test(value)) {
return c.text('Invalid!', 400)
}
})
)
.get('/', (c) => {
return c.text(c.validate('hello world at ' + Date.now()))
})
.get('/invalid', (c) => {
return c.text(c.validate('hello world at ' + NaN))
})

Check warning on line 28 in src/validator/response-validator.sandbox.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.sandbox.ts#L10-L28

Added lines #L10 - L28 were not covered by tests

export default app

Check warning on line 30 in src/validator/response-validator.sandbox.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.sandbox.ts#L30

Added line #L30 was not covered by tests
90 changes: 90 additions & 0 deletions src/validator/response-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Context } from '../context'
import type { Env, MiddlewareHandler, ResponseValidationTargets } from '../types'

type ResponseValidationTargetKeys = keyof ResponseValidationTargets

export type ResponseValidationFunction<
U extends ResponseValidationTargetKeys,
E extends Env = {},
P extends string = string
> = (
value: ResponseValidationTargets[U],
c: Context<E, P>
) => undefined | Response | Promise<Response>

const textRegex = /^text\/([a-z-\.]+\+)?(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/
const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/
const htmlRegex = /^text\/html(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/

export const responseValidator = <
P extends string,
U extends ResponseValidationTargetKeys,
P2 extends string = P,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
E extends Env = any
>(
target: U,
validationFunc: ResponseValidationFunction<U, E, P2>
): MiddlewareHandler<E, P> => {
return async (c, next) => {
await next()

Check warning on line 30 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L26-L30

Added lines #L26 - L30 were not covered by tests

if (!c.finalized) {
return
}

Check warning on line 34 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L32-L34

Added lines #L32 - L34 were not covered by tests

let value: unknown

Check warning on line 36 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L36

Added line #L36 was not covered by tests

const contentType = c.res.headers.get('Content-Type')

Check warning on line 38 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L38

Added line #L38 was not covered by tests

switch (target) {
case 'body':
if (!c.res.body) {
break
}
value = c.res.body
break
case 'text':
if (!contentType || !textRegex.test(contentType) || typeof c.validateData !== 'string') {
break
}
value = c.validateData
break
case 'json':
if (!contentType || !jsonRegex.test(contentType) || typeof c.validateData !== 'object') {
break
}
value = c.validateData
break
case 'html':
if (!contentType || !htmlRegex.test(contentType) || typeof c.validateData !== 'string') {
break
}
value = c.validateData
break
case 'header':
value = Object.fromEntries(c.res.headers.entries()) as Record<string, string>
break
case 'cookie':
value = c.res.headers.getSetCookie().reduce((record, cookie) => {
const [name, ...rest] = cookie.split('=')
record[name] = rest.join('=').split(';')[0]
return record
}, {} as Record<string, string>)
break
case 'status':
value = {
ok: c.res.ok,
status: c.res.status,
statusText: c.res.statusText,
}
break
}

Check warning on line 82 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L40-L82

Added lines #L40 - L82 were not covered by tests

const res = await validationFunc(value, c)

Check warning on line 84 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L84

Added line #L84 was not covered by tests

if (res instanceof Response) {
c.res = res
}
}
}

Check warning on line 90 in src/validator/response-validator.ts

View check run for this annotation

Codecov / codecov/patch

src/validator/response-validator.ts#L86-L90

Added lines #L86 - L90 were not covered by tests
Loading