验证请求
在本章中,我们将首先减少检查请求消息某些属性的代码。我们将使用 protoc-gen-validate
插件来生成某些消息的验证代码。这个插件可以帮助我们在请求消息的属性检查时更加简洁和方便。例如,在检查任务的描述长度和到期日期时,我们可以直接调用生成的 Validate()
函数,它会告诉我们请求消息是否符合要求。
我们将做的第一件事是安装该插件。它是由 Buf 维护的,可以通过以下命令进行安装:
$ go install github.com/envoyproxy/protoc-gen-validate
安装完成后,我们可以使用 protoc
的 --validate_out
选项来生成验证代码。
无论是手动使用 protoc
还是通过 Buf CLI,我们都需要从 GitHub 仓库复制 validate.proto
文件。可以通过以下链接找到该文件: https://github.com/bufbuild/protoc-gen-validate/blob/main/validate/validate.proto 。我们将把它复制到我们的 proto
文件夹中的 validate
目录下:
proto
└── validate
└── validate.proto
接下来,我们可以在其他 proto
文件中导入该文件,并使用提供的验证规则作为字段选项。
我们将以 proto/todo/v2/todo.proto
中的 AddTaskRequest
为例。当前内容如下:
message AddTaskRequest {
string description = 1;
google.protobuf.Timestamp due_date = 2;
}
每次我们尝试添加任务时,服务器端会检查 description
是否为空,以及 due_date
是否大于 time.Now()
。
现在,我们将把这个逻辑编码到我们的 proto
文件中。首先,我们需要导入 validate.proto
文件。然后,我们将能够访问 validate.rules
字段选项,该选项包含了多种类型的规则集合。我们将处理 string
和 Timestamp
类型,并使用 min_len
和 gt_now
字段。第一个规则描述了在调用 Validate
时,字符串应该具有的最小长度;第二个规则则要求提供的 Timestamp
必须是未来的时间。
import "validate/validate.proto";
// ...
message AddTaskRequest {
string description = 1 [
(validate.rules).string.min_len = 1
];
google.protobuf.Timestamp due_date = 2 [
(validate.rules).timestamp.gt_now = true
];
}
一旦我们定义了这些规则,我们就需要生成代码来检查这些规则。否则,这些选项就没有意义。我们将使用 protoc
手动生成代码,然后我会展示如何使用 Buf
和 Bazel
来生成。
正如前面提到的,使用该插件时,可以在 protoc
命令中使用 --validate_out
选项,命令如下:
$ protoc -Iproto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --validate_out="lang=go,paths=source_relative:proto" proto/todo/v2/*.proto
请注意,这个命令与我们之前运行的命令类似。我们只是添加了一个新选项,告诉它在 Go 代码上工作,并根据 v2
文件夹中的 proto
文件生成代码。
现在,在 Protobuf 和 gRPC 生成的代码基础上,您应该还会在 v2 文件夹中看到一个 .pb.validate.go
文件。它应该如下所示:
proto/todo/v2
├── todo.pb.go
├── todo.pb.validate.go
├── todo.proto
└── todo_grpc.pb.go
在生成的 todo.pb.validate.go
文件中,你应该能够看到类似以下的函数:
// Validate checks the field values on Task with the rules
// defined in the proto definition for this message. If any rules are violated,
// the first error encountered is returned, or nil if there are no violations.
func (m *Task) Validate() error {
return m.validate(false)
}
现在,我们将在服务器端的 AddTask
端点中使用这个函数。当前的检查逻辑如下:
func (s *server) AddTask(_ context.Context, in *pb.AddTaskRequest) (*pb.AddTaskResponse, error) {
if len(in.Description) == 0 {
return nil, status.Error(codes.InvalidArgument, "expected a task description, got an empty string")
}
if in.DueDate.AsTime().Before(time.Now().UTC()) {
return nil, status.Error(codes.InvalidArgument, "expected a task due_date that is in the future")
}
//...
}
我们将这部分代码替换为调用 Validate
函数。我们只需要调用 in.Validate()
,如果返回错误,就直接返回该错误;否则,继续执行:
func (s *server) AddTask(_ context.Context, in *pb.AddTaskRequest) (*pb.AddTaskResponse, error) {
if err := in.Validate(); err != nil {
return nil, err
}
//...
}
这样做非常简单,我们避免了手动编写所有的检查逻辑,并且确保在不同的端点中返回一致的错误信息。
接下来,我们可以回到客户端,取消注释错误部分的函数,逐个测试:
func main() {
//...
fmt.Println("-------ERROR-------")
addTask(c, "", dueDate)
addTask(c, "not empty", time.Now().Add(-5*time.Second))
fmt.Println("-------------------")
}
对于第一个 addTask
,我们应该得到如下错误:
$ go run ./client 0.0.0.0:50051
-------ERROR-------
rpc error: code = Unknown desc = invalid AddTaskRequest .Description: value length must be at least 1 runes
对于第二个 addTask
,我们应该得到如下错误:
$ go run ./client 0.0.0.0:50051
-------ERROR-------
rpc error: code = Unknown desc = invalid AddTaskRequest .DueDate: value must be greater than now
注意,错误代码是 Unknown
。截至本书编写时,protoc-gen-validate
插件似乎没有自定义的错误代码,这可能会在插件的 v2
版本中出现。然而,它为我们提供了简单的验证代码和清晰的错误消息。
Buf
使用 Buf
CLI 配合 protoc-gen-validate
非常简单。我们只需要在 YAML 配置文件中添加一些设置来生成代码。首先,我们需要在 buf.yaml
文件中添加对 protoc-gen-validate
的依赖:
version: v1
#...
deps:
- buf.build/envoyproxy/protoc-gen-validate
这告诉 Buf
在生成过程中需要 protoc-gen-validate
,它会自动处理如何拉取该依赖。
接下来,我们需要在 buf.gen.yaml
文件中配置插件:
version: v1
plugins:
#...
- plugin: buf.build/bufbuild/validate-go
out: proto
opt: paths=source_relative
这些选项与我们之前手动输入的相同。现在,我们可以像平常一样运行以下命令来生成代码:
$ buf generate proto
你现在应该得到与使用 protoc
命令生成相同的三个文件:todo.pb.validate.go
、todo_grpc.pb.go
和 todo.pb.go
。需要注意的是,在这种情况下,我们还生成了 v1
版本的代码和 validate.proto
文件。
Bazel
如同往常一样,首先我们需要在 WORKSPACE.bazel
文件中定义依赖项。我们将从 GitHub 获取 protoc-gen-validate
项目,并加载其相关依赖:
以下代码引用了一个名为 |
#...
git_repository(
name = "com_envoyproxy_protoc_gen_validate",
tag = PROTOC_GEN_VALIDATE_VERSION,
remote = "https://github.com/bufbuild/protoc-gen-validate"
)
load("@com_envoyproxy_protoc_gen_validate//bazel:repositories.bzl", "pgv_dependencies")
load("@com_envoyproxy_protoc_gen_validate//:dependencies.bzl", "go_third_party")
pgv_dependencies()
# gazelle:repository_macro deps.bzl%go_third_party
go_third_party()
现在,我们需要更新 deps.bzl
文件中的依赖项。我们可以通过输入以下命令来完成:
$ bazel run //:gazelle-update-repos
最后,我们需要生成代码并将其链接到 proto/todo/v2/BUILD.bazel
中现有的 todo go_library
。
首先,在 v2_proto
的 proto_library
中添加对 protoc-gen-validate validate.proto
的依赖。这将允许 todo.proto
导入它:
proto_library(
name = "v2_proto",
#...
deps = [
#...
"@com_envoyproxy_protoc_gen_validate//validate:validate_proto",
],
)
然后,我们将用 pgv_go_proto_library
替换 v2_go_proto
的 go_proto_library
(pgv
代表 protoc-gen-validate
)。除此之外,我们还将添加对 protoc-gen-validate
库的依赖,以便生成的代码能够访问编译所需的任何 protoc-gen-validate
内部代码:
load("@com_envoyproxy_protoc_gen_validate//bazel:pgv_proto_library.bzl", "pgv_go_proto_library")
#...
pgv_go_proto_library(
name = "v2_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_grpc"],
importpath = "github.com/PacktPublishing/gRPC-Go-for-Professionals/proto/todo/v2",
proto = ":v2_proto",
deps = ["@com_envoyproxy_protoc_gen_validate//validate:validate_go"],
)
最后,为了避免下次运行 Gazelle 时导入 validate/validate.proto
的路径发生歧义,我们将其映射到 @com_envoyproxy_protoc_gen_validate//validate:validate_proto
(在 protoc-gen-validate
中定义)。我们可以在 proto/todo/v2/BUILD.bazel
文件的顶部添加以下 Gazelle 指令:
# gazelle:resolve proto validate/validate.proto
@com_envoyproxy_protoc_gen_validate//validate:validate_proto
现在,我们用 pgv_go_proto_library
替换了旧的 v2_go_proto
,依赖此库的代码将自动访问生成的 Validate
函数。
我们可以尝试运行服务器:
$ bazel run //server:server 0.0.0.0:50051
listening at 0.0.0.0:50051
然后运行客户端,并取消注释错误部分的代码:
$ bazel run //client:client 0.0.0.0:50051
-------ERROR-------
rpc error: code = Unknown desc = invalid AddTaskRequest
.Description: value length must be at least 1 runes
总结:我们看到了如何在 proto
文件中编码验证逻辑,并通过 protoc-gen-validate
自动生成验证代码。这使得我们的代码更加简洁,并为我们的 API 端点提供了一致的错误消息。