使用protobuf validate自定义报错内容

使用validate限制输入

在一些http框架中,都可以直接引入validate,对入参进行限制和校验,例如gin框架

gin

要将请求正文绑定到类型中,请使用模型绑定。我们目前支持 JSON、XML、YAML 和标准表格值(foo=bar&boo=baz)的绑定。

Gin 使用 go-playground/validator/v10 进行验证。点击此处查看有关标签用法的完整文档。

请注意,您需要在所有要绑定的字段上设置相应的绑定标签。例如,从 JSON 绑定时,设置 json: "fieldname"

此外,Gin 还提供了两套绑定方法:

  • Type - Must bind
    • 方法 - Bind, BindJSON, BindXML, BindQuery, BindYAML
    • 表现 - 这些方法在gin.context下使用 MustBindWith。如果出现绑定错误,请求将通过 c.AbortWithError(400, err).SetType(ErrorTypeBind) 中止。这会将响应状态代码设置为 400,并将 Content-Type 标头设置为 text/plain;charset=utf-8。请注意,如果在此之后尝试设置响应代码,将会出现警告 [GIN-debug] [WARNING] Headers were already written.想用 422 覆盖状态代码 400。如果您希望对行为有更大的控制权,请考虑使用 ShouldBind 同等方法。
  • Type - Should bind
    • 方法 - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • 表现 - 这些方法在gin.context下使用 ShouldBindWith。如果出现绑定错误,则会返回错误信息,开发人员有责任妥善处理请求和错误。

使用绑定方法时,Gin 会根据 Content-Type 标头来推断绑定内容。如果确定要绑定的内容,可以使用 MustBindWithShouldBindWith

您还可以指定特定字段为必填字段。如果某个字段使用 binding: "required" 修饰,但在绑定时值为空,则会返回错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
router := gin.Default()

// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>manu</user>
// <password>123</password>
// </root>)
router.POST("/loginXML", func(c *gin.Context) {
var xml Login
if err := c.ShouldBindXML(&xml); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if xml.User != "manu" || xml.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Example for binding a HTML form (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// This will infer what binder to use depending on the content-type header.
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})

// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}

请求示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl -v -X POST \
http://localhost:8080/loginJSON \
-H 'content-type: application/json' \
-d '{ "user": "manu" }'
> POST /loginJSON HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
> content-type: application/json
> Content-Length: 18
>
* upload completely sent off: 18 out of 18 bytes
< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=utf-8
< Date: Fri, 04 Aug 2017 03:51:31 GMT
< Content-Length: 100
<
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

跳过验证

使用上述 curl 命令运行上述示例时,返回错误。因为示例中的密码使用了 binding: "required"。如果对密码使用 binding:"-",再次运行上述示例时就不会返回错误。

但是现在普遍使用微服务架构,通过protocol buffer,使用RPC或者gRPC框架实现服务之间交互,这种情况下,参数传递也需要实现参数校验。

protoc-gen-validate (PGV)

PGV 是一个用于生成多语言消息验证器的 protoc 插件。虽然protocol buffers能有效保证结构化数据的类型,但它们无法执行值的语义规则。该插件为 protoc-generated 代码添加了验证此类约束的支持。

开发人员导入 PGV 扩展,并在他们的 proto 文件中用约束规则注释消息和字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package examplepb;

import "validate/validate.proto";

message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];

string email = 2 [(validate.rules).string.email = true];

string name = 3 [(validate.rules).string = {
pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",
max_bytes: 256,
}];

Location home = 4 [(validate.rules).message.required = true];

message Location {
double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}];
double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}];
}
}

使用 PGV 和目标语言的默认插件执行 protoc 会在生成的类型上创建验证方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
p := new(Person)

err := p.Validate() // err: Id must be greater than 999
p.Id = 1000

err = p.Validate() // err: Email must be a valid email address
p.Email = "[email protected]"

err = p.Validate() // err: Name must match pattern '^[^\d\s]+( [^\d\s]+)*$'
p.Name = "Protocol Buffer"

err = p.Validate() // err: Home is required
p.Home = &Location{37.7, 999}

err = p.Validate() // err: Home.Lng must be within [-180, 180]
p.Home.Lng = -122.4

err = p.Validate() // err: nil

使用方法

前提:

  • go 工具链(≥ v1.7)
  • $PATH 中的 protoc 编译器
  • $PATH 中的 protoc-gen-validate
  • 目标语言的官方特定语言插件
  • 目前仅支持 proto3 语法,计划支持 proto2 语法。

安装

从GitHub发布下载

GitHub Releases,然后将插件添加到 $PATH

从源代码构建

1
2
# fetches this repo into $GOPATH
go get -d github.com/envoyproxy/protoc-gen-validate

💡 Yes, our go module path is github.com/envoyproxy/protoc-gen-validate not bufbuild this is intentional.

Changing the module path is effectively creating a new, independent module. We would prefer not to break our users. The Go team are working on better cmd/go support for modules that change paths, but progress is slow. Until then, we will continue to use the envoyproxy module path.

1
2
3
git clone github.com/bufbuild/protoc-gen-validate
# installs PGV into $GOPATH/bin
cd protoc-gen-validate && make build

参数

  • lang:指定要生成的目标语言。目前,仅支持以下选项
    • go
    • c++ 的 cc(已部分实现)
    • java
  • 注意:Python 通过运行时代码生成工作。没有编译时生成。详见 Python 部分。

示例

Go

Go 生成应与官方插件的输出路径相同。对于原语文件 example.proto,相应的验证代码会生成到 ../generated/example.pb.validate.go

1
2
3
4
5
6
protoc \
-I . \
-I path/to/validate/ \
--go_out=":../generated" \
--validate_out="lang=go:../generated" \
example.proto

生成的所有信息都包括以下方法:

  • Validate() 错误会返回验证过程中遇到的第一个错误。
  • ValidateAll() 错误,返回验证过程中遇到的所有错误。

PGV 不需要现有生成代码的额外运行时依赖性。

注意:默认情况下,example.pb.validate.go 嵌套在与您的 go_package 选项名称相匹配的目录结构中。您可以使用 protoc 参数 paths=source_relative:.../generated 来更改,如 --validate_out="lang=go,paths=source_relative:.../generated" 。然后,--validate_out 将在预期的位置输出文件。更多信息,请参阅 Googleprotobuf 文档或软件包和输入路径参数

描述module=example.com/foo 标记的支持。

在较新的 Buf CLI 版本(>v1.9.0)中,可以使用新的插件密钥来代替直接使用 protoc 命令:

1
2
3
4
5
6
# buf.gen.yaml

version: v1
plugins:
- plugin: buf.build/bufbuild/validate-go
out: gen
1
2
3
4
5
# proto/buf.yaml

version: v1
deps:
- buf.build/envoyproxy/protoc-gen-validate

更多语言:

  • Java
  • Python

更多约束:Constraint Rules

拦截器

对于这类相同的报错,可以直接使用中间件,不需要在代码层面对每一次调用进行错误判断处理

中间件包:https://github.com/grpc-ecosystem/go-grpc-middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
grpcSrv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
// Order matters e.g. tracing interceptor have to create span first for the later exemplars to work.
otelgrpc.UnaryServerInterceptor(),
srvMetrics.UnaryServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)),
logging.UnaryServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)),
selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)),
recovery.UnaryServerInterceptor(recovery.WithRecoveryHandler(grpcPanicRecoveryHandler)),
),
grpc.ChainStreamInterceptor(
otelgrpc.StreamServerInterceptor(),
srvMetrics.StreamServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)),
logging.StreamServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)),
selector.StreamServerInterceptor(auth.StreamServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)),
recovery.StreamServerInterceptor(recovery.WithRecoveryHandler(grpcPanicRecoveryHandler)),
),

validate中间件包:https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/validator

示例服务端代码,中间件引入

1
2
3
4
5
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_validator.UnaryServerInterceptor(),
),
grpc.Creds(serverCert))

一元拦截器

1
2
3
4
5
6
7
8
9
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateOpts(opts)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if err := validate(ctx, req, o.shouldFailFast, o.onValidationErrCallback); err != nil {
return nil, err
}
return handler(ctx, req)
}
}

也可以自己写专属自己的拦截器。

Next Generation

📣更新:新一代 protoc-gen-validate(现称为 protovalidate)已发布测试版,适用于 GolangPythonJavaC++!我们还在努力开发 TypeScript 实现。要了解更多信息,请查看我们的博文。我们非常重视您在完善我们产品方面的意见和建议,因此请随时在 protovalidate.** 上分享您的反馈意见。

protovalidate

https://github.com/bufbuild/protovalidate

protovalidate 是一系列库,旨在根据用户定义的验证规则在运行时验证 Protobuf 消息。它由谷歌通用表达式语言(CEL)提供支持,为定义和评估自定义验证规则提供了灵活高效的基础。protovalidate 的主要目标是帮助开发人员确保整个网络的数据一致性和完整性,而不需要生成代码。

protovalidate 是 protoc-gen-validate 的精神继承者。

用法

导入 protovalidate

要在 Protobuf 消息中定义约束条件,请在 .proto 文件中导入 buf/validate/validate.proto

1
2
3
4
5
syntax = "proto3";

package my.package;

import "buf/validate/validate.proto";

通过 buf 构建

在模块的 buf.yaml 中添加对 buf.build/bufbuild/protovalidate 的依赖:

1
2
3
4
5
version: v1
# <snip>
deps:
- buf.build/bufbuild/protovalidate
# <snip>

修改完 buf.yaml 后,别忘了运行 buf mod update,以确保您的依赖项是最新的。

通过protoc构建

在调用 protoc 时,添加指向 proto/protovalidate 目录内容的导入路径(-I 标志):

1
2
3
protoc \
-I ./vendor/protovalidate/proto/protovalidate \
# <snip>

可以看到,与PGV不同的是,PGV通过protoc会生成单独的go代码文件,而protovalidate不会。

实施验证约束

可以使用 buf.validate Protobuf 软件包强制执行验证约束。规则直接在 .proto 文件中指定。

例如:

  1. String 字段验证:对于基本的User信息,我们可以强制执行一些限制条件,如用户姓名的最小长度。

    1
    2
    3
    4
    5
    6
    7
    8
    syntax = "proto3";

    import "buf/validate/validate.proto";

    message User {
    // User's name, must be at least 1 character long.
    string name = 1 [(buf.validate.field).string.min_len = 1];
    }
  2. Map 字段验证:对于带有Product数量映射的产品信息,我们可以确保所有数量都是正数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    syntax = "proto3";

    import "google/protobuf/timestamp.proto";
    import "buf/validate/validate.proto";

    message User {
    // User's creation date must be in the past.
    google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];
    }
  3. 常用类型 (WKT) 验证:对于User信息,我们可以添加一个约束条件,以确保 created_at 时间戳在过去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    syntax = "proto3";

    import "google/protobuf/timestamp.proto";
    import "buf/validate/validate.proto";

    message User {
    // User's creation date must be in the past.
    google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];
    }

对于更高级或自定义的限制,protovalidate 允许使用 CEL 表达式,将信息整合到各个字段中。

  1. 字段级表达式:我们可以强制要求以字符串形式发送的产品price包含"$""£"等货币符号。我们要确保价格为正数,货币符号有效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    syntax = "proto3";

    import "buf/validate/validate.proto";

    message Product {
    string price = 1 [(buf.validate.field).cel = {
    id: "product.price",
    message: "Price must be positive and include a valid currency symbol ($ or £)",
    expression: "(this.startsWith('$') || this.startsWith('£')) && double(this.substring(1)) > 0"
    }];
    }
  2. 报文级表达式:对于Transaction消息,我们可以使用消息级 CEL 表达式来确保delivery_date总是在purchase_date之后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    syntax = "proto3";

    import "google/protobuf/timestamp.proto";
    import "buf/validate/validate.proto";

    message Transaction {
    google.protobuf.Timestamp purchase_date = 1;
    google.protobuf.Timestamp delivery_date = 2;

    option (buf.validate.message).cel = {
    id: "transaction.delivery_date",
    message: "Delivery date must be after purchase date",
    expression: "this.delivery_date > this.purchase_date"
    };
    }
  3. 在表达式中生成错误信息:我们可以直接在 CEL 表达式中生成自定义错误信息。在本例中,如果age小于 18 岁,CEL 表达式将评估为错误信息字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    syntax = "proto3";

    import "buf/validate/validate.proto";

    message User {
    int32 age = 1 [(buf.validate.field).cel = {
    id: "user.age",
    expression: "this.age < 18 ? 'User must be at least 18 years old': ''"
    }];
    }

验证信息

一旦信息注释了限制条件,就可以使用支持的语言库之一进行验证;无需额外生成代码。使用PGV相同的中间件代码即可。

文档

protovalidate 为验证 Protobuf 消息提供了一个强大的框架,它可以对各种数据类型执行标准和自定义约束,并在出现验证违规时提供详细的错误信息。要详细了解其所有组件、支持的约束条件以及如何有效使用它们,请参阅的综合文档。主要组件包括

  • 标准约束protovalidate 支持所有字段类型的各种标准约束,以及 Protobuf Wellnown-Type 的特殊功能。你可以将这些约束应用到你的 Protobuf 消息中,以确保它们满足某些常见条件。
  • 自定义约束:利用 Google 的通用表达式语言 (CEL),protovalidate 允许您创建复杂的自定义约束,以处理字段和消息级别的标准约束未涵盖的独特验证场景。
  • 错误处理:当发生违规时,protovalidate 会提供详细的错误信息,帮助您快速确定问题来源并进行修复。

protoc-gen-validate

protovalidateprotoc-gen-validate 的精神继承者,它提供了与原始插件相同的所有功能,无需自定义代码生成,并具有在 CEL 中描述复杂约束的新能力

protovalidate 的约束条件与 protoc-gen-validate 中的约束条件非常接近,以确保开发人员能轻松过渡。要从 protoc-gen-validate 迁移到 protovalidate,请使用提供的迁移工具逐步升级您的.proto文件。

迁移向导

protoc-gen-validate 迁移到 protovalidate 应该是安全、渐进和相对无痛的,但仍然需要进行一些操作才能实现。为了减轻这一负担,我们提供了本文档和迁移工具,以简化迁移过程。

迁移工具,可以将将所有热PGV约束改为使用protovalidate约束,避免一条一条更改

命令汇总:

1
2
3
4
5
6
7
8
9
10
11
12
// 先保证已经格式化,避免出现一些奇怪的代码

// 添加注释
go run ./tools/protovalidate-migrate -w /path/to/protos

// 更新生成的PGV代码,一般可以直接删除

// (可选)替代注释
go run ./tools/protovalidate-migrate -w --replace-protovalidate /path/to/protos

// 删除PGV的代码
go run ./tools/protovalidate-migrate -w --remove-legacy /path/to/protos

更多更详细的用法,可参考官方文档:https://github.com/bufbuild/protovalidate/tree/main/tools/protovalidate-migrate

自定义报错

使用protovalidate最大的原因是protovalidate支持自定义报错,可以替代掉PGV的默认报错,具体用法:

例如一个注册服务,需要验证用户输入的用户名和昵称,并且报错由自己自定义,返回给用户更明确的报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
message SignupReq {
string user_name = 1 [(buf.validate.field).cel = {
id: "user_name",
message: "用户名只能由中文、英文字母、数字和下划线组成,并且长度在20以内",
expression: "this.matches(\"^[\u4e00-\u9fa5a-zA-Z0-9_]{1,20}$\")",
}]; // 用户名,判断是否由汉字, 大小写字母,0-9数组,下划线组成
string password = 2; // 密码
string nick_name = 3 [(buf.validate.field).cel = {
id: "nick_name",
message: "昵称只能由中文、英文字母、数字、下划线以及英文括号和中文括号组成,并且长度在20以内",
expression: "this.matches(\"^[\u4e00-\u9fa5a-zA-Z0-9_().()]{1,20}$\")",
}];
string phone = 4 [(buf.validate.field).string = { pattern: "^1[3|4|5|7|8][0-9]{9}$", len: 11 }, (buf.validate.field).ignore_empty = true]; // 联系方式
string address = 5; // 联系地址
string email = 6 [(buf.validate.field).string.email = true]; // 邮箱地址
string verify_code_id = 7; // 验证码序号
string verify_code = 8; // 验证码
}

通过自定义的中间件统一返回中文报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package server

import (
"context"
"errors"

"github.com/bufbuild/protovalidate-go"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)

type Validate struct {
v *protovalidate.Validator
}

func NewValidate() (*Validate, error) {
v, err := protovalidate.New()
if err != nil {
return nil, err
}
return &Validate{v: v}, nil
}

func (v *Validate) validateUnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
switch req.(type) {
case proto.Message:
if err := v.v.Validate(req.(proto.Message)); err != nil {
var valErr *protovalidate.ValidationError
if ok := errors.As(err, &valErr); ok && len(valErr.ToProto().GetViolations()) > 0 {
return nil, status.Error(codes.InvalidArgument, valErr.ToProto().GetViolations()[0].Message)
}
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
}

优势

总结下来,protovalidate相比PGV而言,有很多优势:

  • 不需要生成额外代码
  • CEL通用表示语言有很强大的扩展性,而且带有很多函数,实现逻辑判断

CEL 通用表示语言

通用表达式语言(CEL)是一种非图灵完备语言,旨在实现简单、快速、安全和可移植性。CEL 类似 C 语言的语法与 C++、Go、Java 和 TypeScript 中的等效表达式几乎完全相同。

这里为了更好的使用protovalidate,列举一些常用的CEL表达式

字符串长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//`len` dictates that the field value must have the specified
//number of characters (Unicode code points), which may differ from the number
//of bytes in the string. If the field value does not meet the specified
//length, an error message will be generated.
//
//```proto
//message MyString {
// // value length must be 5 characters
// string value = 1 [(buf.validate.field).string.len = 5];
//}
//```
optional uint64 len = 19 [(priv.field).cel = {
id: "string.len",
expression: "uint(this.size()) != rules.len ? 'value length must be %s characters'.format([rules.len]) : ''"
}];

特定字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//`in` specifies that the field value must be equal to one of the specified
//values. If the field value isn't one of the specified values, an error
//message will be generated.
//
//```proto
//message MyString {
// // value must be in list ["apple", "banana"]
// repeated string value = 1 [(buf.validate.field).string.in = "apple", (buf.validate.field).string.in = "banana"];
//}
//```
repeated string in = 10 [(priv.field).cel = {
id: "string.in",
expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''"
}];

通过说明可以看到,继承自PGV的约束,在protovalidate中,其实是通过CEL实现,不得不再次感叹Google在扩展性和兼容性上展现出的强大实力。

更多CEL示例,可以查看protoc文件

https://github.com/bufbuild/protovalidate/blob/main/proto/protovalidate/buf/validate/validate.proto

一些实践

由于正则表达式使用 RE2 语法,并且是以 CEL 语言写在一个字符串中,因此,中间会会很多的转义。

例如

  1. 账号 由数字/字母/特殊字符(~!@#$%^&*()_+`-={}|[];’:”,./<>?·)组成,最大支持100个字符,不支持空格

    1
    2
    3
    4
    5
    6
    7
    string user_name = 1 [
    (buf.validate.field).cel = {
    id: "user_name",
    message: "账号格式不符合规范",
    expression: "this.matches(\"^[\\\\w~!@#\\\\$%\\\\^&\\\\*\\\\(\\\\)_+`\\\\-={}|\\\\[\\\\];':\\\",./<>?·]{1,100}$\")",
    }
    ]; // 账号 由数字/字母/特殊字符(~!@#$%^&*()_+`-={}|[];':",./<>?·)组成,最大支持100个字符,不支持空格
  2. 用户名、昵称由汉字/数字/字母/特殊字符(~!@#$%^&*()_+`-={}|[];’:”,./<>?·)组成,最大支持100个字符,两边不允许有空格,中间支持空格

    1
    2
    3
    4
    5
    6
    7
    string nick_name = 3 [
    (buf.validate.field).cel = {
    id: "nick_name",
    message: "用户名/昵称格式不符合规范",
    expression: "this.matches(\"^[^\\\\s][\\u4e00-\\u9fa5\\\\w~!@#\\\\$%\\\\^&\\\\*\\\\(\\\\)_+`\\\\-={}|\\\\[\\\\];':\\\",./<>?·\\\\s]{1,100}[^\\\\s]$\")",
    }
    ]; // 用户名、昵称由汉字/数字/字母/特殊字符(~!@#$%^&*()_+`-={}|[];':",./<>?·)组成,最大支持100个字符,两边不允许有空格,中间支持空格

上面这两个也是踩了很多坑,取巧的方法是直接询问 ChatGPT

1
2
3
4
5
以下是一个CEL语法的声明 
```
expression: "this.matches(\"reg\")",
```
其中`reg`是一个正则表达式,使用RE2语法,一个由汉字/数字/字母/特殊字符(~!@#$%^&*()_+`-={}|[];':",./<>?·)组成,最大支持100个字符,两边不允许有空格,中间支持空格的正则表达式,请完善这个声明。
1
2
3
4
5
6
7
以下是完善后的声明:

```
expression: "this.matches(\"[\\u4e00-\\u9fa5\\w~!@#$%^&*()_+`\\-={}|\\[\\];':\",./<>?·]{1,100}\")"
```

这个声明使用了Unicode编码范围来匹配汉字、数字、字母、特殊字符(~!@#$%^&*()_+`-={}|[];':",./<>?·),并且限制了最大字符数为100个。两边不允许有空格,中间可以有空格。
1
2
3
4
出现报错
```
compilation error: failed to compile expression nick_name: ERROR: <input>:1:14: Syntax error: token recognition error at: '\"[\一-\龥\\w'\n | this.matches(\"[\一-\龥\\w~!@#$%^&*()_+`\\-={}|\\[\\];':\",./<>?·]{1,100}\")\n | .............^\nERROR: <input>:1:31: Syntax error: token recognition error at: '~'\n | this.matches(\"[\一-\龥\\w~!@#$%^&*()_+`\\-={}|\\[\\];':\",./<>?·]{1,100}\")\n | ..............................^\nERROR: <input>:1:33: Syntax error: token recognition error at: '@'\n | this.matches(\"[\一-\龥\\w~!@#$%^&*()_+`\\-={}|\\[\\];':\",./<>?·]{1,100}
```
1
2
3
4
5
6
7
非常抱歉,我之前的回答有一些错误。在正则表达式中,需要对一些特殊字符进行转义。以下是修正后的声明:

```
expression: "this.matches(\"[\\u4e00-\\u9fa5\\w~!@#$%^&*()_+`\\-={}|\\[\\];':\\\",./<>?·]{1,100}\")"
```

这个修正后的声明使用了正确的转义字符,并且限制了最大字符数为100个。两边不允许有空格,中间可以有空格。请尝试使用修正后的声明。

。。。

其实 ChatGPT 的回答也不是完全正确,还需要进一步调试。但是使用 ChatGPT 确实是一个很好的解决思路。

推荐阅读

Model binding and validation

Go gRPC Middleware

protovalidate

protoc-gen-validate

protovalidate-go