feat: introduce zod-validator (#16)

* feat: introduce zod-validator

* fixed `package.json`

* changeset
pull/18/head
Yusuke Wada 2023-01-02 00:03:44 +09:00 committed by GitHub
parent 80592fe7db
commit 58e42aa8da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 293 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/zod-validator': patch
---
first release

11
.vscode/settings.json vendored 100644
View File

@ -0,0 +1,11 @@
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

View File

@ -6,7 +6,9 @@
"packages/*"
],
"scripts": {
"build": "yarn workspace @hono/hello build"
"build:hello": "yarn workspace @hono/hello build",
"build:zod-validator": "yarn workspace @hono/zod-validator build",
"build": "yarn build:hello && yarn:zod-validator"
},
"license": "MIT",
"private": true,

View File

@ -0,0 +1,34 @@
# Zod validator middleware for Hono
**WIP**
The validator middleware using [Zod](https://zod.dev) for [Hono](https://honojs.dev) applications.
You can write a schema with Zod and validate the incoming values.
## Usage
```ts
import { z } from 'zod'
import { zValidator } from '../src'
const schema = z.object({
name: z.string(),
age: z.number(),
})
app.post('/author', zValidator('json', schema), (c) => {
const data = c.req.valid()
return c.json({
success: true,
message: `${data.name} is ${data.age}`,
})
})
```
## Author
Yusuke Wada <https://github.com/yusukebe>
## License
MIT

View File

@ -0,0 +1 @@
module.exports = require('../../jest.config.js')

View File

@ -0,0 +1,38 @@
{
"name": "@hono/zod-validator",
"version": "0.0.0",
"description": "Validator middleware using Zod",
"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",
"private": false,
"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.0.0-rc.3",
"zod": "^3.19.1"
},
"devDependencies": {
"hono": "3.0.0-rc.3",
"zod": "3.19.1"
}
}

View File

@ -0,0 +1,33 @@
import type { Context } from 'hono'
import { validator } from 'hono/validator'
import type { z } from 'zod'
import type { ZodSchema, ZodError } from 'zod'
type ValidationTypes = 'json' | 'form' | 'query' | 'queries'
type Hook<T> = (
result: { success: true; data: T } | { success: false; error: ZodError },
c: Context
) => Response | Promise<Response> | void
export const zValidator = <T extends ZodSchema, Type extends ValidationTypes>(
type: Type,
schema: T,
hook?: Hook<z.infer<T>>
) =>
validator(type, (value, c) => {
const result = schema.safeParse(value)
if (hook) {
const hookResult = hook(result, c)
if (hookResult instanceof Response || hookResult instanceof Promise<Response>) {
return hookResult
}
}
if (!result.success) {
return c.json(result, 400)
}
const data = result.data as z.infer<T>
return data
})

View File

@ -0,0 +1,132 @@
import { Hono } from 'hono'
import type { Equal, Expect } from 'hono/utils/types'
import { z } from 'zod'
import { zValidator } from '../src'
describe('Basic', () => {
const app = new Hono()
const schema = z.object({
name: z.string(),
age: z.number(),
})
const route = app
.post('/author', zValidator('json', schema), (c) => {
const data = c.req.valid()
return c.jsonT({
success: true,
message: `${data.name} is ${data.age}`,
})
})
.build()
type Actual = typeof route
type Expected = {
post: {
'/author': {
input: {
json: {
name: string
age: number
}
}
output: {
json: {
success: boolean
message: string
}
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type verify = Expect<Equal<Actual, Expected>>
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 = z.object({
id: z.number(),
title: z.string(),
})
app.post(
'/post',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.text('Invalid!', 400)
}
const data = result.data
return c.text(`${data.id} is valid!`)
}),
(c) => {
const data = c.req.valid()
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)
})
})

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"declaration": false,
"outDir": "./dist/cjs"
}
}

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"declaration": true,
"outDir": "./dist/esm"
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
},
"include": [
"src/**/*.ts"
],
}

View File

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

View File

@ -2550,6 +2550,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hono@3.0.0-rc.3:
version "3.0.0-rc.3"
resolved "https://registry.yarnpkg.com/hono/-/hono-3.0.0-rc.3.tgz#8f4cfc03816114bc3541c1802f0c65e0730327e8"
integrity sha512-j5GLwfTb9smHcneQ8hm1I7bwnLGxKHAZYuCOZuq+CGpEg/SrWW0niIiuhLE4FIIFAfeL3vUsjy0BzOWS6Hur0w==
hono@^2.3.0, hono@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/hono/-/hono-2.6.2.tgz#1e32a4b4b3bf557a8aa87e392b00b505a1ec1fdd"
@ -4723,3 +4728,8 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@3.19.1:
version "3.19.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473"
integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==