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: hertz binding refactor #795

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 285 additions & 11 deletions content/zh/docs/hertz/tutorials/basic-feature/binding-and-validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {

...

// Bind
// Bind 只做参数绑定
req = Test{}
err = ctx.Bind(&req)

Expand All @@ -41,6 +41,22 @@ func main() {
...
}
```
### 全部 API
> hertz version >= v0.7.0

| API | 说明 |
|:--------------------|:----------------------------------------------------------------------------------------------------------------------------------------|
| ctx.BindAndValidate | 利用下述的 go-tag 进行参数绑定,并在绑定成功后做一次参数校验(如果有校验 tag 的话) |
| ctx.Bind | 同 "BindAndValidate" 但是不做参数校验 |
| ctx.BindQuery | 绑定所有 Query 参数,相当于给每一个 field 声明一个 'query' tag,适用于没写 tag 的场景 |
| ctx.BindHeader | 绑定所有 Header 参数,相当于给每一个 field 声明一个 'header' tag,适用于没写 tag 的场景 |
| ctx.BindPath | 绑定所有 Path 参数,相当于给每一个 field 声明一个 'path' tag,适用于没写 tag 的场景 |
| ctx.BindForm | 绑定所有 Form 参数,相当于给每一个 field 声明一个 'form' tag,需要 Content-Type 为: `application/x-www-form-urlencoded`/`multipart/form-data`, 适用于没写 tag 的场景 |
| ctx.BindJSON | 绑定 JSON Body,调用 json.Unmarshal() 进行反序列化,需要 Body 为 `application/json` 格式 |
| ctx.BindProtobuf | 绑定 Protobuf Body,调用 proto.Unmarshal() 进行反序列化,需要 Body 为 `application/x-protobuf` 格式 |
| ctx.BindByContentType | 根据 Content-Type 来自动选择绑定的方法,其中 "GET" 请求会调用 BindQuery(), 带有 Body 的请求会根据 Content-Type 自动选择 |
| ctx.Validate | 进行参数校验,需要校验 tag 配合使用(默认校验 tag: vd) |


## 支持的 tag 及参数绑定优先级

Expand Down Expand Up @@ -76,18 +92,172 @@ path > form > query > cookie > header > json > raw_body

``` go
type TagRequiredReq struct {
// 当 JSON 中没有 hertz 字段时,会返回 required 错误:binding: expr_path=hertz, cause=missing required parameter
// 当 JSON 中没有 hertz 字段时,会返回 required 错误
Hertz string `json:"hertz,required"`
// 当 query 和 JSON 中同时没有 kitex 字段时,会返回 required 错误:binding: expr_path=hertz, cause=missing required parameter"
// 当 query 和 JSON 中同时没有 kitex 字段时,会返回 required 错误
Kitex string `query:"kitex,required" json:"kitex,required" `
}
```

## 常见用法
## 常用配置
> hertz 在 v0.7.0 版本对参数绑定&校验进行重构,重构后配置的行为发生变更,下面将分别介绍<br>
> 如果还想使用之前的绑定器,目前已把其实现放到了 hertz-contrib 下,可通过自定义 binder 引入
### 自定义 binder
> hertz version >= v0.7.0 支持

需要实现 Binder 接口,并通过配置方式注入到 hertz engine
```go
type Binder interface {
Name() string // 绑定器的名字
// 下面为各种绑定方法
Bind(*protocol.Request, interface{}, param.Params) error
BindAndValidate(*protocol.Request, interface{}, param.Params) error
BindQuery(*protocol.Request, interface{}) error
BindHeader(*protocol.Request, interface{}) error
BindPath(*protocol.Request, interface{}, param.Params) error
BindForm(*protocol.Request, interface{}) error
BindJSON(*protocol.Request, interface{}) error
BindProtobuf(*protocol.Request, interface{}) error
}
```
注入
```go

func main() {
// 通过配置的方式注入自定义 binder
h := server.New(server.WithCustomBinder(&mockBinder{}))
...
h.Spin()
}


type mockBinder struct{}

func (m *mockBinder) Name() string {
return "test binder"
}

func (m *mockBinder) Bind(request *protocol.Request, i interface{}, params param.Params) error {
return nil
}

func (m *mockBinder) BindAndValidate(request *protocol.Request, i interface{}, params param.Params) error {
return fmt.Errorf("test binder")
}

func (m *mockBinder) BindQuery(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindHeader(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindPath(request *protocol.Request, i interface{}, params param.Params) error {
return nil
}

func (m *mockBinder) BindForm(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindJSON(request *protocol.Request, i interface{}) error {
return nil
}

func (m *mockBinder) BindProtobuf(request *protocol.Request, i interface{}) error {
return nil
}

```

目前已拓展的绑定器:
* bytedance/go-tagexpr: https://github.com/hertz-contrib/binding/tree/main/go_tagexpr

### 自定义 validator
> hertz version >= v0.7.0 支持

需要实现 Validator 接口,并通过配置方式注入到 hertz engine
```go
type StructValidator interface {
ValidateStruct(interface{}) error // 校验函数
Engine() interface{} // 返回底层的 Validator
ValidateTag() string // 校验的 tag, 声明校验器使用的 tag
}
```
注入
```go

func main() {
// 通过配置的方式注入自定义 binder
h := server.New(server.WithCustomValidator(&mockValidator{}))
...
h.Spin()
}

type mockValidator struct{}

func (m *mockValidator) ValidateStruct(interface{}) error {
return fmt.Errorf("test mock validator")
}

func (m *mockValidator) Engine() interface{} {
return nil
}

func (m *mockValidator) ValidateTag() string {
return "vt"
}

```
目前已拓展的校验器:
* go-playground/validator: https://github.com/hertz-contrib/binding/tree/main/go_playground

### 自定义 bind 和 validate 的 Error

绑定参数发生错误和参数校验失败的时候,用户可以自定义的 Error([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_error) ),使用方法如下:
绑定参数发生错误和参数校验失败的时候,用户可以自定义 Error 的内容,使用方法如下:<br>
**hertz version >= v0.7.0**
> 暂不支持自定义 bind error

自定义错误 error:
```go
package main
import (
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server"
)

type ValidateError struct {
ErrType, FailField, Msg string
}

// Error implements error interface.
func (e *ValidateError) Error() string {
if e.Msg != "" {
return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
}
return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
}

func main() {
validateConfig := &binding.ValidateConfig{}
validateConfig.SetValidatorErrorFactory(func(failField, msg string) error {
err := ValidateError{
ErrType: "validateErr",
FailField: "[validateFailField]: " + failField,
Msg: "[validateErrMsg]: " + msg,
}

return &err
})
h := server.New(server.WithValidateConfig(validateConfig))
...
h.Spin()
}
```

**hertz version < v0.7.0**<br>
[demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_error)

```go
import "github.com/cloudwego/hertz/pkg/app/server/binding"
Expand Down Expand Up @@ -142,8 +312,48 @@ func init() {
```

### 自定义类型解析
在参数绑定的时候, 针对某些特殊类型, 默认行为无法满足需求, 可使用自定义类型解析来解决, 使用方法如下:<br>
**hertz version >= v0.7.0**<br>
```go
package main

import (
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server"
)

type Nested struct {
B string
C string
}

type TestBind struct {
A Nested `query:"a,required"`
}

在参数绑定的时候,所有的 request 参数都是 `string` 或者 `[]string`;当有一些 field 的类型为非基础类型或者无法直接通过 `string` 转换,则可以自定义类型解析([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_type_resolve) )。使用方法如下:
func main() {
bindConfig := &binding.BindConfig{}
// v0.7.0 重构后,在原基础上增加了请求 Request 内容以及路由参数,可方便用户更加灵活的自定义类型解析
bindConfig.MustRegTypeUnmarshal(reflect.TypeOf(Nested{}), func(req *protocol.Request, params param.Params, text string) (reflect.Value, error) {
if text == "" {
return reflect.ValueOf(Nested{}), nil
}
val := Nested{
B: text[:5],
C: text[5:],
}
// 此外,也可以利用 req, params 来获取其他参数进行参数绑定
return reflect.ValueOf(val), nil
})
h := server.New(server.WithBindConfig(bindConfig))

...
h.Spin()
}
```
**hertz version < v0.7.0**<br>

[demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_type_resolve)

```go
import "github.com/cloudwego/hertz/pkg/app/server/binding"
Expand Down Expand Up @@ -172,9 +382,39 @@ func init() {
```

### 自定义验证函数
可以通过注册自定义验证函数,在'vd'注解中实现复杂的验证逻辑:<br>
**hertz version >= v0.7.0**<br>
```go
package main

可以通过注册自定义验证函数,在'vd'注解中实现复杂的验证逻辑([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_validate_func) ),使用方法如下:
import (
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
type Req struct {
A int `query:"a" vd:"test($)"`
}
validateConfig := &binding.ValidateConfig{}
validateConfig.MustRegValidateFunc("test", func(args ...interface{}) error {
if len(args) != 1 {
return fmt.Errorf("the args must be one")
}
s, _ := args[0].(string)
if s == "123" {
return fmt.Errorf("the args can not be 123")
}
return nil
})
h := server.New(server.WithValidateConfig(validateConfig))
...
h.Spin()
}
```

**hertz version < v0.7.0**<br>
[demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_validate_func)
```go
import "github.com/cloudwego/hertz/pkg/app/server/binding"

Expand All @@ -194,8 +434,25 @@ func init() {

### 配置 looseZero

在一些场景下,前端有时候传来的信息只有 key 没有 value,这会导致绑定数值类型的时候,会报错 `cause=parameter type does not match binding data`。
这时需要配置 looseZero 模式([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/loose_zero) ),使用方法如下:
在一些场景下,前端有时候传来的信息只有 key 没有 value,这会导致绑定数值类型的时候报错;这时需要配置 looseZero 模式,使用方法如下:
**hertz version >= v0.7.0**<br>
```go
package main

import (
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
bindConfig := binding.NewBindConfig()
bindConfig.LooseZeroMode = true
h := server.New(server.WithBindConfig(bindConfig))
...
h.Spin()
}
```
**hertz version < v0.7.0**<br>

```go
import "github.com/cloudwego/hertz/pkg/app/server/binding"
Expand All @@ -207,9 +464,24 @@ func init() {
```

### 配置其他 json unmarshal 库
在绑定参数的时候,如果请求体为 json,会进行一次 json 的 unmarshal,如果用户需要使用特定的 json 库可以自己配置(hertz 默认使用开源 json 库 [sonic](https://github.com/bytedance/sonic) )。使用方法如下:<br>
**hertz version >= v0.7.0**<br>
```go
import (
"github.com/cloudwego/hertz/pkg/app/server/binding"
"github.com/cloudwego/hertz/pkg/app/server"
)

在绑定参数的时候,如果请求体为 json,会进行一次 json 的 unmarshal,如果用户需要使用特定的 json 库可以自己配置(hertz 默认使用开源 json 库 [sonic](https://github.com/bytedance/sonic) )。使用方法如下:

func main() {
bindConfig := binding.NewBindConfig()
bindConfig.UseStdJSONUnmarshaler() // 使用标准库, hertz 默认使用 sonic 作为 json 序列化/反序列化器
//bindConfig.UseThirdPartyJSONUnmarshaler(sonic.Unmarshal) // 使用 sonic 作为 json 序列化/反序列化器
h := server.New(server.WithBindConfig(bindConfig))
...
h.Spin()
}
```
**hertz version < v0.7.0**<br>
```go
import "github.com/cloudwego/hertz/pkg/app/server/binding"

Expand All @@ -226,6 +498,7 @@ func init() {
```

### 设置默认值
> 重构前后使用方式都一样

参数支持 "default" tag 进行默认值的配置,使用方法如下:

Expand All @@ -237,6 +510,7 @@ type UserInfoResponse struct {
```

### 绑定文件
> 重构前后使用方式一样,IDL 场景不支持文件绑定

参数绑定支持绑定文件,使用方法如下:

Expand Down