protoc-gen-validator

Validate 通过 protoc-gen-validator 生成 Validate() 接口,减少非业务代码的编写。

概述

服务运行中,经常需要和其他客户端进行数据交互,验证来自客户端的数据的合法性。但是这些代码编写起来枯燥且易错,vine 提供 protoc-gen-validator 工具来自动生成结构体 Validate() 方法,减少一部分代码的手动编写。

使用

1.先编写 validator.proto 文件

message Person {
  // +gen:min_len=4
  // +gen:max_len=10
  string name = 1;

  // +gen:required;gt=10;lt=100
  int32 age = 2;

  // +gen:required;min_bytes=3;max_bytes=4;
  bytes any = 3;

  // +gen:email
  string email = 4;
}

2.安装 protoc-gen-validator

go get github.com/vine-io/vine/cmd/protoc-gen-validator

3.生成 Validate() 方法

protoc -I=$GOPATH/src --gogo_out=:.  --validator_out=:. proto/validator.proto

执行完成后生成以下代码:

func (m *Person) Valdiate() error {
    return m.ValidateE("")
}

func (m *Person) ValidateE() error {
	errs := make([]error, 0)
	if len(m.Name) != 0 {
		if !(len(m.Name) >= 4) {
			errs = append(errs, errors.New("field 'name' length must less than '4'"))
		}
		if !(len(m.Name) <= 10) {
			errs = append(errs, errors.New("field 'name' length must great than '10'"))
		}
	}
	if int64(m.Age) == 0 {
		errs = append(errs, errors.New("field 'age' is required"))
	} else {
		if !(m.Age < 100) {
			errs = append(errs, errors.New("field 'age' must less than '100'"))
		}
		if !(m.Age > 10) {
			errs = append(errs, errors.New("field 'age' must great than '10'"))
		}
	}
	if len(m.Any) == 0 {
		errs = append(errs, errors.New("field 'any' is required"))
	} else {
		if !(len(m.Any) <= 3) {
			errs = append(errs, errors.New("field 'any' length must less than '3'"))
		}
		if !(len(m.Any) >= 4) {
			errs = append(errs, errors.New("field 'any' length must great than '4'"))
		}
	}
	if len(m.Email) != 0 {
		if !is.Email(m.Email) {
			errs = append(errs, errors.New("field 'email' is not a valid email"))
		}
	}
	return is.MargeErr(errs...)
}

4.验证

func main() {
 	p := pb.Person{}
	p.Age = 1
	p.Email = "11"
 	err := p.Validate()
 	log.Printf("%v\n", err)
}
// output:  field 'age' must great than '10';field 'any' is required;field 'email' is not a valid email

多个错误时,使用 ; 隔开

语法解析

protoc-gen-validator 通过解析 protobuf 中的注释来生成 Validate() 规则。

// +gen:ignore
message Struct {
    // +gen:required
    string field1 = 1;
    
    // +gen:required;email
    // +gen:min_len=3
    string field2 = 2;
}

语法规则

有效的注释有以下的规则:

  • 注释必须以 +gen 作为开头
  • 注释的内容必须紧贴对应的字段,中间不能有空行
  • 支持多行注释,也可以将多行合并成一行,并用 ; 作为分隔符

类型支持

message 类型规则:

  • ignore: 忽略该 message ,不生成 Validate() 方法
// +gen:ignore
message P {

}

message 作为内嵌字段时支持的规则:

  • required: 判断该字段是否为 nil。
message P {
    // +gen:required
    Sub sub = 1;
}

message Sub {

}

注: 在引用外部的 message 时,请确认 message 存在 Validate() 方法

string 类型支持的规则

  • required: 判断是否为空
  • default: 字段为空时指定的默认值,(不可用 required 同时使用)
  • in, enum: 判断字段的值是否存在于指定的列表中
  • not_in: 判断字段的值是否在指定的列表之外
  • min_len: 指定字段的最小长度
  • max_len: 指定字段的最大长度
  • prefix: 判断字段是否以给定的值为开头
  • suffix: 判断字段是否以给定的值为结尾
  • contains: 判断字段是否包含给定的值
  • pattern: 判断该字段是否为有效的正则表达式
  • number: 判断该字段是否为有效数字
  • email: 判断该字段是否为有效的邮箱地址
  • ip: 判断该字段是否为有效的 ip 地址
  • ipv4: 判断该字段是否为有效的 ipv4
  • ipv6: 判断该字段是否为有效的 ipv6
  • crontab: 判断该字段是否为有效的 crontab 表达式
  • uuid: 判断该字段是否为有效的 uuid v4
  • uri: 判断该字段是否为有效的 uri
  • domain: 判断该字段是否为有效的域名
message S {
    // +gen:required
    // +gen:default="hello"
    // +gen:in=["1", "2", "3"]
    // +gen:enum=["a", "b", "c"]
    // +gen:not_in=["d", "s"]
    // +gen:min_len=3
    // +gen:min_max=4
    // +gen:prefix="http"
    // +gen:suffix=".com"
    // +gen:contains="www"
    // +gen:pattern=`\d+(\w+){3,5}`
    // +gen:number
    // +gen:ip
    // +gen:ipv4
    // +gen:ipv6
    // +gen:crontab
    // +gen:uuid
    // +gen:uri
    // +gen:domain
    string m = 1;
}

注: string pattern 最好单独一行,以免和其他规则冲突

数字类型的支持,包含 int32, int64, fixed32, fix64, float, double

  • required: 判断是否为 0
  • default: 字段为空时指定的默认值,(不可用 required 同时使用)
  • in, enum: 判断字段的值是否存在于指定的列表中
  • not_in: 判断字段的值是否在指定的列表之外
  • lt: 指定字段小于指定值
  • lte: 指定字段的小于等于指定值
  • gt: 指定字段大于指定值
  • gte: 指定字段大于等于指定值
message S {
    // +gen:required
    float a = 1;

    // +gen:default=3.14
    double pi = 2

    // +gen:in=[1,2,3]
    // +gen:enum=[2,3]
    // +gen:not_in=[4,5]
    int32 b = 3;

    // +gen:ge=3
    // +ggen:gte=4
    // +gen:lte=9
    // +gen:lt=10
    int64 c = 4;
}

bytes 类型的支持:

  • required: 判断字段的长度是否为0
  • min_bytes: 判断字段的最小字节数是否大于给定值
  • max_bytes: 判断字段的最大字节数是否小于给定值
message S {
    // +gen:required
    // +gen:min_bytes=10
    // +gen:min_bytes=1024
    bytes any = 1;
}

repeated 类型的支持:repeated 类型的字段在 golang 中会被解析成切片。

  • required: 判断切片的长度是否为0
  • min_len: 判断切片的最小长度是否大于给定值
  • max_len: 判断切片的最大长度是否小于给定值
message S {
    // +gen:required
    // +gen:min_len=3
    // +gen:max_len=5
    repeated string = 1;
}

内嵌 message

protoc-gen-validator 支持解析内嵌 message, 使用 +gen:inline 可以将子 message 中的字段嵌入该 message。实例如下:

message Meta {
  // +gen:required
  string name = 1;
  // +gen:required
  int64 id = 2;
}

message Request {
  // +gen:inline
  // +gen:required
  Meta meta = 1;
  string data = 2;
}

解析出来的内容如下:


func (m *Meta) Validate() error {
	return m.ValidateE("")
}

func (m *Meta) ValidateE(prefix string) error {
	errs := make([]error, 0)
	if len(m.Name) == 0 {
		errs = append(errs, fmt.Errorf("field '%sname' is required", prefix))
	}
	if int64(m.Id) == 0 {
		errs = append(errs, fmt.Errorf("field '%sid' is required", prefix))
	}
	return is.MargeErr(errs...)
}

func (m *Request) Validate() error {
	return m.ValidateE("")
}

func (m *Request) ValidateE(prefix string) error {
	errs := make([]error, 0)
	if len(m.Name) == 0 {
		errs = append(errs, fmt.Errorf("field '%sname' is required", prefix))
	}
	if int64(m.Id) == 0 {
		errs = append(errs, fmt.Errorf("field '%sid' is required", prefix))
	}
	return is.MargeErr(errs...)
}

func (m *Response) Validate() error {
	return m.ValidateE("")
}

func (m *Response) ValidateE(prefix string) error {
	errs := make([]error, 0)
	return is.MargeErr(errs...)
}

最后修改 August 27, 2021: 完整文档结构 (be4076c)