Skip to content

Commit

Permalink
add unknownutil validator
Browse files Browse the repository at this point in the history
  • Loading branch information
ryoppippi committed Oct 6, 2023
1 parent 152e93e commit fd8b492
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/ci-unknwnutil-validator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-unknownutil-validator
on:
push:
branches: [main]
paths:
- 'packages/unknownutil-validator/**'
pull_request:
branches: ['*']
paths:
- 'packages/unknownutil-validator/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/unknownutil-validator
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build:typebox-validator": "yarn workspace @hono/typebox-validator build",
"build:medley-router": "yarn workspace @hono/medley-router build",
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
"build:unknownutil-validator": "yarn workspace @hono/unknownutil-validator build",
"build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build",
"build:typia-validator": "yarn workspace @hono/typia-validator build",
"build": "run-p build:*"
Expand Down
2 changes: 2 additions & 0 deletions packages/unknownutil-validator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# @hono/unknownutil-validator

46 changes: 46 additions & 0 deletions packages/unknownutil-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Unknownutil validator middleware for Hono

The type validator middleware using [Unknownutil](https://github.com/lambdalisue/deno-unknownutil) for [Hono](https://honojs.dev) applications.
You can write a schema with unknownutil and check the type of the incoming values.

## Usage

```ts
import { is } from 'unknownutil'
import { uValidator } from '@hono/unknownutil-validator'

const schema = is.ObjectOf({
name: is.String,
age: is.Number,
})

app.post('/author', uValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.name} is ${data.age}`,
})
})
```

Hook:

```ts
app.post(
'/post',
uValidator('json', schema, (result, c) => {
if (result.error) {
return c.text('Invalid!', 400)
}
})
//...
)
```

## Author

Ryotaro "Justin" Kimura <https://github.com/ryoppippi>

## License

MIT
1 change: 1 addition & 0 deletions packages/unknownutil-validator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../jest.config.js')
37 changes: 37 additions & 0 deletions packages/unknownutil-validator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@hono/unknownutil-validator",
"version": "0.1.0",
"description": "Validator middleware using unknonwnutil",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "jest",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"build": "rimraf dist && yarn build:cjs && yarn build:esm",
"prerelease": "yarn build && yarn test",
"release": "yarn publish"
},
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": "3.*",
"unknownutil": "^3.9.0"
},
"devDependencies": {
"hono": "^3.1.0",
"unknownutil": "^3.9.0"
}
}
60 changes: 60 additions & 0 deletions packages/unknownutil-validator/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Context, MiddlewareHandler, Env, ValidationTargets, TypedResponse } from 'hono'
import { validator } from 'hono/validator'
import { AssertError, ensure, type Predicate, type PredicateType } from 'unknownutil'

export type Hook<T, E extends Env, P extends string> = (
result: { data: T; error: undefined } | { data: undefined; error: AssertError },
c: Context<E, P>
) => Response | Promise<Response> | void | Promise<Response | void> | TypedResponse<T>

export const uValidator = <
T,
S extends Predicate<T>,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
V extends {
in: { [K in Target]: PredicateType<S> }
out: { [K in Target]: PredicateType<S> }
} = {
in: { [K in Target]: PredicateType<S> }
out: { [K in Target]: PredicateType<S> }
}
>(
target: Target,
schema: S,
hook?: Hook<PredicateType<S>, E, P>
): MiddlewareHandler<E, P, V> =>
validator(target, (value, c) => {
let resultUnion: [AssertError, undefined] | [undefined, PredicateType<S>]

try {
resultUnion = [undefined, ensure(value, schema) as PredicateType<S>]
} catch (error: unknown) {
if (error instanceof AssertError) {
resultUnion = [error, undefined]
} else {
throw new Error(undefined, { cause: error })
}
}

const [error, data] = resultUnion

if (hook) {
const hookResult = hook(error ? { data: undefined, error } : { data, error: undefined }, c)
if (hookResult) {
if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult
}
if ('response' in hookResult) {
return hookResult.response
}
}
}

if (error) {
return c.json({ success: false, error: error.message }, 400)
}

return value
})
131 changes: 131 additions & 0 deletions packages/unknownutil-validator/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types'
import { is } from 'unknownutil'
import { uValidator } from '../src'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never

describe('Basic', () => {
const app = new Hono()

const schema = is.ObjectOf({
name: is.String,
age: is.Number,
})

const route = app.post('/author', uValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.jsonT({
success: true,
message: `${data.name} is ${data.age}`,
})
})

type Actual = ExtractSchema<typeof route>
type Expected = {
'/author': {
$post: {
input: {
json: {
name: string
age: number
}
}
output: {
success: true
message: string
}
}
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type verify = Expect<Equal<Expected, Actual>>

it('Should return 200 response', async () => {
const req = new Request('http://localhost/author', {
body: JSON.stringify({
name: 'Superman',
age: 20,
}),
method: 'POST',
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
success: true,
message: 'Superman is 20',
})
})

it('Should return 400 response', async () => {
const req = new Request('http://localhost/author', {
body: JSON.stringify({
name: 'Superman',
age: '20',
}),
method: 'POST',
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
const data = (await res.json()) as { success: boolean }
expect(data['success']).toBe(false)
})
})

describe('With Hook', () => {
const app = new Hono()

const schema = is.ObjectOf({
id: is.Number,
title: is.String,
})

app.post(
'/post',
uValidator('json', schema, (result, c) => {
if (result.error) {
return c.text('Invalid!', 400)
}
const data = result.data
return c.text(`${data.id} is valid!`)
}),
(c) => {
const data = c.req.valid('json')
return c.json({
success: true,
message: `${data.id} is ${data.title}`,
})
}
)

it('Should return 200 response', async () => {
const req = new Request('http://localhost/post', {
body: JSON.stringify({
id: 123,
title: 'Hello',
}),
method: 'POST',
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(await res.text()).toBe('123 is valid!')
})

it('Should return 400 response', async () => {
const req = new Request('http://localhost/post', {
body: JSON.stringify({
id: '123',
title: 'Hello',
}),
method: 'POST',
})
const res = await app.request(req)
expect(res).not.toBeNull()
expect(res.status).toBe(400)
})
})
8 changes: 8 additions & 0 deletions packages/unknownutil-validator/tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"declaration": false,
"outDir": "./dist/cjs"
}
}
8 changes: 8 additions & 0 deletions packages/unknownutil-validator/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"declaration": true,
"outDir": "./dist/esm"
}
}
9 changes: 9 additions & 0 deletions packages/unknownutil-validator/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
},
"include": [
"src/**/*.ts"
],
}

0 comments on commit fd8b492

Please sign in to comment.