[][buf] protoc-gen-validate (PGV)
[!重要] protoc-gen-validate (PGV) 已达到稳定状态,并处于维护模式。
我们建议新项目和现有项目过渡到使用 [
protovalidate
][pv]。如果你想了解更多关于 protoc-gen-validate 的局限性以及我们如何设计 [
protovalidate
][pv] 来改进它,请阅读[我们的博客文章][pv-announce]。
PGV 是一个 protoc 插件,用于生成多语言消息验证器。虽然 protocol buffers 能有效地保证结构化数据的类型,但它们无法强制执行值的语义规则。这个插件为 protoc 生成的代码添加了支持,以验证这些约束。
开发人员导入 PGV 扩展,并在他们的 proto 文件中用约束规则注释消息和字段:
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: "^[A-Za-z]+( [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
将在生成的类型上创建 Validate
方法:
p := new(Person)
err := p.Validate() // 错误:Id 必须大于 999
p.Id = 1000
err = p.Validate() // 错误:Email 必须是有效的电子邮件地址
p.Email = "example@bufbuild.com"
err = p.Validate() // 错误:Name 必须匹配模式 '^[A-Za-z]+( [A-Za-z]+)*$'
p.Name = "Protocol Buffer"
err = p.Validate() // 错误:Home 是必需的
p.Home = &Location{37.7, 999}
err = p.Validate() // 错误:Home.Lng 必须在 [-180, 180] 范围内
p.Home.Lng = -122.4
err = p.Validate() // 错误:nil
使用方法
依赖项
go
工具链(≥ v1.7)$PATH
中的protoc
编译器$PATH
中的protoc-gen-validate
- 目标语言的官方特定语言插件
- 目前仅支持
proto3
语法。 计划支持proto2
语法。
安装
从 GitHub 发布版下载
从 GitHub 发布版 下载资产,解压缩并将插件添加到 $PATH
中。
从源代码构建
# 将此仓库获取到 $GOPATH 中
go get -d github.com/envoyproxy/protoc-gen-validate
💡 是的,我们的 go 模块路径是
github.com/envoyproxy/protoc-gen-validate
而不是bufbuild
,这是有意为之。更改模块路径实际上是创建一个新的、独立的模块。我们不希望破坏我们的用户。Go 团队正在努力为路径更改的模块提供更好的
cmd/go
支持,但进展缓慢。在此之前,我们将继续使用envoyproxy
模块路径。
git clone https://github.com/bufbuild/protoc-gen-validate.git
# 将 PGV 安装到 $GOPATH/bin
cd protoc-gen-validate && make build
参数
lang
:指定要生成的目标语言。目前,唯一支持的选项是:go
cc
(用于 C++,部分实现)java
- 注意:Python 通过运行时代码生成工作。没有编译时生成。有关详细信息,请参阅 Python 部分。
示例
Go
Go 生成应该与官方插件输出到相同的输出路径。对于 proto 文件 example.proto
,相应的验证代码生成到 ../generated/example.pb.validate.go
:
protoc \
-I . \
-I path/to/validate/ \
--go_out=":../generated" \
--validate_out="lang=go:../generated" \
example.proto
所有生成的消息都包含以下方法:
Validate() error
,在验证过程中返回遇到的第一个错误。ValidateAll() error
,返回验证过程中遇到的所有错误。
PGV 不需要从现有生成的代码中添加任何额外的运行时依赖项。
注意:默认情况下,example.pb.validate.go 嵌套在与你的 option go_package
名称匹配的目录结构中。你可以使用 protoc 参数 paths=source_relative:.
来更改这一点,比如 --validate_out="lang=go,paths=source_relative:../generated"
。然后 --validate_out
将在预期的位置输出文件。有关更多信息,请参阅 Google 的 protobuf 文档或包和输入路径或参数。
还支持 module=example.com/foo
标志,如此处所述。
使用较新的 Buf CLI 版本(>v1.9.0),你可以使用新的插件键,而不是直接使用 protoc
命令:
# buf.gen.yaml
version: v1
plugins:
- plugin: buf.build/bufbuild/validate-go
out: gen
# proto/buf.yaml
version: v1
deps:
- buf.build/envoyproxy/protoc-gen-validate
Java
Java 生成与现有的 Java 项目 protobuf 工具链集成。对于 Maven 项目,在你的 pom.xml 或 build.gradle 中添加以下内容。
<dependencies>
<dependency>
<groupId>build.buf.protoc-gen-validate</groupId>
<artifactId>pgv-java-stub</artifactId>
<version>${pgv.version}</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.1.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
</protocArtifact>
</configuration>
<executions>
<execution>
<id>protoc-java-pgv</id>
<goals>
<goal>compile-custom</goal>
</goals>
<configuration>
<pluginParameter>lang=java</pluginParameter>
<pluginId>java-pgv</pluginId>
<pluginArtifact>
build.buf.protoc-gen-validate:protoc-gen-validate:${pgv.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
plugins {
...
id "com.google.protobuf" version "${protobuf.version}"
...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protoc.version}"
}
plugins {
javapgv {
artifact = "build.buf.protoc-gen-validate:protoc-gen-validate:${pgv.version}"
}
}
generateProtoTasks {
all()*.plugins {
javapgv {
option "lang=java"
}
}
}
}
// 创建一个反射加载生成的验证器的验证器索引
ValidatorIndex index = new ReflectiveValidatorIndex();
// 断言消息是有效的
index.validatorFor(message.getClass()).assertValid(message);
// 创建 gRPC 客户端和服务器拦截器以自动验证消息(需要 pgv-java-grpc 模块)
clientStub = clientStub.withInterceptors(new ValidatingClientInterceptor(index));
serverBuilder.addService(ServerInterceptors.intercept(svc, new ValidatingServerInterceptor(index)));
Python
Python 实现通过即时代码生成工作。换句话说,validate(msg)
函数是按需编写并 exec-ed 的。LRU 缓存通过存储每个描述符的生成函数来提高性能。
Python 包可在 PyPI 上获得。
要运行 validate()
,请执行以下操作:
from entities_pb2 import Person
from protoc_gen_validate.validator import validate, ValidationFailed
p = Person(first_name="Foo", last_name="Bar", age=42)
try:
validate(p)
except ValidationFailed as err:
print(err)
你可以使用 print_validate()
函数查看生成的代码。
约束规则
提供的约束 主要是根据 JSON Schema 中的约束建模的。PGV 规则可以为同一字段混合使用;插件确保应用于字段的规则在代码生成之前不会相互矛盾。
查看约束规则比较矩阵以了解特定语言的约束能力。
数字
所有数字类型(
float
、double
、int32
、int64
、uint32
、uint64
、sint32
、sint64
、fixed32
、fixed64
、sfixed32
、sfixed64
)共享相同的规则。
-
const:字段必须精确地等于指定值。
// x 必须精确等于 1.23 float x = 1 [(validate.rules).float.const = 1.23];
-
lt/lte/gt/gte:这些不等式(分别为
<
、<=
、>
、>=
)允许推导字段必须位于的范围。// x 必须小于 10 int32 x = 1 [(validate.rules).int32.lt = 10]; // x 必须大于或等于 20 uint64 x = 1 [(validate.rules).uint64.gte = 20]; // x 必须在 [30, 40) 范围内 fixed32 x = 1 [(validate.rules).fixed32 = {gte:30, lt: 40}];
反转
lt(e)
和gt(e)
的值是有效的,并创建一个排除范围。// x 必须在 [30, 40) 范围之外 double x = 1 [(validate.rules).double = {lt:30, gte:40}];
-
in/not_in:这两个规则允许为字段的值指定允许/拒绝列表。
// x 必须是 1、2 或 3 uint32 x = 1 [(validate.rules).uint32 = {in: [1,2,3]}]; // x 不能是 0 或 0.99 float x = 1 [(validate.rules).float = {not_in: [0, 0.99]}];
-
ignore_empty:此规则指定如果字段为空或设置为默认值,则忽略任何验证规则。这些通常在更新请求中能够取消设置字段时很有用,或者在切换到 WKT 不可行的情况下跳过可选字段的验证。
uint32 x = 1 [(validate.rules).uint32 = {ignore_empty: true, gte: 200}];
布尔值
-
const:字段必须精确地等于指定值。
// x 必须设置为 true bool x = 1 [(validate.rules).bool.const = true]; // x 不能设置为 true bool x = 1 [(validate.rules).bool.const = false];
字符串
-
const:字段必须精确地等于指定值。
// x 必须是一个有效的主机名(根据 RFC 1034) string x = 1 [(validate.rules).string.hostname = true];
// x 必须是一个有效的 IP 地址(IPv4 或 IPv6) string x = 1 [(validate.rules).string.ip = true];
// x 必须是一个有效的 IPv4 地址 // 例如: "192.168.0.1" string x = 1 [(validate.rules).string.ipv4 = true];
// x 必须是一个有效的 IPv6 地址 // 例如: "fe80::3" string x = 1 [(validate.rules).string.ipv6 = true];
// x 必须是一个有效的绝对 URI (根据 RFC 3986) string x = 1 [(validate.rules).string.uri = true];
// x 必须是一个有效的 URI 引用(绝对或相对) string x = 1 [(validate.rules).string.uri_ref = true];
// x 必须是一个有效的 UUID (根据 RFC 4122) string x = 1 [(validate.rules).string.uuid = true];
// x 必须符合 HTTP 头名称的已知正则表达式(根据 RFC 7230) string x = 1 [(validate.rules).string.well_known_regex = HTTP_HEADER_NAME]
// x 必须符合 HTTP 头值的已知正则表达式(根据 RFC 7230) string x = 1 [(validate.rules).string.well_known_regex = HTTP_HEADER_VALUE];
// x 必须符合头部的已知正则表达式,不允许 \r\n\0 字符。 string x = 1 [(validate.rules).string {well_known_regex: HTTP_HEADER_VALUE, strict: false}];
### 字节
> 字面值应该使用字符串表示,必要时使用转义。
- **const**: 字段必须完全等于指定的值。
```protobuf
// x 必须设置为 "foo" ("\x66\x6f\x6f")
bytes x = 1 [(validate.rules).bytes.const = "foo"];
// x 必须设置为 "\xf0\x90\x28\xbc"
bytes x = 1 [(validate.rules).bytes.const = "\xf0\x90\x28\xbc"];
-
len/min_len/max_len: 这些规则限制字段中的字节数。
// x 必须正好是 3 个字节 bytes x = 1 [(validate.rules).bytes.len = 3]; // x 必须至少是 3 个字节长 bytes x = 1 [(validate.rules).bytes.min_len = 3]; // x 必须在 5 到 10 个字节之间,包含边界值 bytes x = 1 [(validate.rules).bytes = {min_len: 5, max_len: 10}];
-
pattern: 字段必须匹配指定的 [RE2 兼容][re2] 正则表达式。包含的表达式应省略任何分隔符(即
/\d+/
应该只写成\d+
)。// x 必须是非空的 ASCII 字节序列 bytes x = 1 [(validate.rules).bytes.pattern = "^[\x00-\x7F]+$"];
-
prefix/suffix/contains: 字段必须包含指定的字节序列,可以选择指定位置。
// x 必须以 "\x99" 开头 bytes x = 1 [(validate.rules).bytes.prefix = "\x99"]; // x 必须以 "buz\x7a" 结尾 bytes x = 1 [(validate.rules).bytes.suffix = "buz\x7a"]; // x 必须在任意位置包含 "baz" bytes x = 1 [(validate.rules).bytes.contains = "baz"];
-
in/not_in: 这两个规则允许为字段的值指定允许/拒绝列表。
// x 必须是 "foo"、"bar" 或 "baz" 之一 bytes x = 1 [(validate.rules).bytes = {in: ["foo", "bar", "baz"]}]; // x 不能是 "fizz" 或 "buzz" bytes x = 1 [(validate.rules).bytes = {not_in: ["fizz", "buzz"]}];
-
ignore_empty: 此规则指定如果字段为空或设置为默认值,则忽略任何验证规则。这通常在更新请求中可以取消设置字段,或者在无法切换到 WKT 的可选字段上跳过验证时很有用。
bytes x = 1 [(validate.rules).bytes = {ignore_empty: true, in: ["foo", "bar", "baz"]}];
-
常见格式: 这些规则为常见模式提供高级约束。这些约束通常比等效的正则表达式模式更宽松和高效,同时提供更具解释性的失败描述。
// x 必须是字节格式的有效 IP 地址(IPv4 或 IPv6) bytes x = 1 [(validate.rules).bytes.ip = true]; // x 必须是字节格式的有效 IPv4 地址 // 例如: "\xC0\xA8\x00\x01" bytes x = 1 [(validate.rules).bytes.ipv4 = true]; // x 必须是字节格式的有效 IPv6 地址 // 例如: "\x20\x01\x0D\xB8\x85\xA3\x00\x00\x00\x00\x8A\x2E\x03\x70\x73\x34" bytes x = 1 [(validate.rules).bytes.ipv6 = true];
枚举
所有字面值应使用枚举描述符中定义的数字(int32)值。
以下示例使用此 State
枚举
enum State {
INACTIVE = 0;
PENDING = 1;
ACTIVE = 2;
}
-
const: 字段必须完全等于指定的值。
// x 必须设置为 ACTIVE (2) State x = 1 [(validate.rules).enum.const = 2];
-
defined_only: 字段必须是枚举描述符中指定的值之一。
// x 只能是 INACTIVE、PENDING 或 ACTIVE State x = 1 [(validate.rules).enum.defined_only = true];
-
in/not_in: 这两个规则允许为字段的值指定允许/拒绝列表。
// x 必须是 INACTIVE (0) 或 ACTIVE (2) State x = 1 [(validate.rules).enum = {in: [0,2]}]; // x 不能是 PENDING (1) State x = 1 [(validate.rules).enum = {not_in: [1]}];
消息
如果字段包含一个消息,并且该消息是使用 PGV 生成的,将递归执行验证。未使用 PGV 生成的消息将被跳过。
// 如果 Person 是用 PGV 生成的,并且 x 被设置,
// x 的字段将被验证。
Person x = 1;
-
skip: 此规则指定不应评估此字段的验证规则。
// Person x 的字段将不会被验证。 Person x = 1 [(validate.rules).message.skip = true];
-
required: 此规则指定字段不能未设置。
// x 不能未设置 Person x = 1 [(validate.rules).message.required = true]; // x 不能未设置,但不会对 x 执行验证 Person x = 1 [(validate.rules).message = {required: true, skip: true}];
重复
-
min_items/max_items: 这些规则控制字段中包含的元素数量
// x 必须至少包含 3 个元素 repeated int32 x = 1 [(validate.rules).repeated.min_items = 3]; // x 必须包含 5 到 10 个 Person,包含边界值 repeated Person x = 1 [(validate.rules).repeated = {min_items: 5, max_items: 10}]; // x 必须恰好包含 7 个元素 repeated double x = 1 [(validate.rules).repeated = {min_items: 7, max_items: 7}];
-
unique: 此规则要求字段中的所有元素必须是唯一的。此规则不支持重复的消息。
// x 必须包含唯一的 int64 值 repeated int64 x = 1 [(validate.rules).repeated.unique = true];
-
items: 此规则指定应该应用于字段中每个元素的约束。重复的消息字段也会应用其验证规则,除非在此约束上指定了
skip
。// x 必须包含正浮点值 repeated float x = 1 [(validate.rules).repeated.items.float.gt = 0]; // x 必须包含 Person,但不验证它们 repeated Person x = 1 [(validate.rules).repeated.items.message.skip = true];
-
ignore_empty: 此规则指定如果字段为空或设置为默认值,则忽略任何验证规则。这通常在更新请求中可以取消设置字段,或者在无法切换到 WKT 的可选字段上跳过验证时很有用。
repeated int64 x = 1 [(validate.rules).repeated = {ignore_empty: true, items: {int64: {gt: 200}}}];
映射
-
min_pairs/max_pairs: 这些规则控制此字段中包含的键值对数量
// x 必须至少包含 3 个键值对 map<string, uint64> x = 1 [(validate.rules).map.min_pairs = 3]; // x 必须包含 5 到 10 个键值对 map<string, string> x = 1 [(validate.rules).map = {min_pairs: 5, max_pairs: 10}]; // x 必须恰好包含 7 个键值对 map<string, Person> x = 1 [(validate.rules).map = {min_pairs: 7, max_pairs: 7}];
-
no_sparse: 对于具有消息值的映射字段,将此规则设置为 true 会禁止具有未设置值的键。
// x 中的所有值都必须设置 map<uint64, Person> x = 1 [(validate.rules).map.no_sparse = true];
-
keys: 此规则指定应用于字段中键的约束。
// x 的键必须全部为负数 <sint32, string> x = [(validate.rules).map.keys.sint32.lt = 0];
-
values: 此规则指定应用于字段中每个值的约束。重复的消息字段也会应用其验证规则,除非在此约束上指定了
skip
。// x 必须包含至少 3 个字符的字符串 map<string, string> x = 1 [(validate.rules).map.values.string.min_len = 3]; // x 必须包含 Person,但不验证它们 map<string, Person> x = 1 [(validate.rules).map.values.message.skip = true];
-
ignore_empty: 此规则指定如果字段为空或设置为默认值,则忽略任何验证规则。这通常在更新请求中可以取消设置字段,或者在无法切换到 WKT 的可选字段上跳过验证时很有用。
map<string, string> x = 1 [(validate.rules).map = {ignore_empty: true, values: {string: {min_len: 3}}}];
众所周知的类型(WKTs)
一组 [WKTs][wkts] 与 protoc 打包在一起,是许多领域中有用的常见消息模式。
标量值包装器
在 proto3
语法中,无法区分未设置和标量字段的零值。值 WKTs 通过将它们包装在消息中来允许这种区分。PGV 允许使用包装器封装的相同标量规则。
// 如果设置了,x 必须大于 3
google.protobuf.Int32Value x = 1 [(validate.rules).int32.gt = 3];
消息规则也可以与标量众所周知的类型(WKTs)一起使用:
// 确保如果没有为 age 设置值,尽管其零值为 0,也不会通过验证。
message X {google.protobuf.Int32Value age = 1 [(validate.rules).int32.gt = -1, (validate.rules).message.required = true];}
Any
-
required: 此规则指定必须设置字段
// x 不能未设置 google.protobuf.Any x = 1 [(validate.rules).any.required = true];
-
in/not_in: 这两个规则允许为此字段中的
type_url
值指定允许/拒绝列表。如果可能,请考虑使用oneof
联合而不是in
。// x 不能是 Duration 或 Timestamp WKT google.protobuf.Any x = 1 [(validate.rules).any = {not_in: [ "type.googleapis.com/google.protobuf.Duration", "type.googleapis.com/google.protobuf.Timestamp" ]}
// x必须在[epoch, 2009/11/10T23:00:00Z)范围内 google.protobuf.Timestamp x = 1 [(validate.rules).timestamp = { gte: {}, lt: {seconds: 63393490800} }];
将`lt(e)`和`gt(e)`的值互换是有效的,并创建一个排除范围。
```protobuf
// x必须在[epoch, 2009/11/10T23:00:00Z)范围之外
google.protobuf.Timestamp x = 1 [(validate.rules).timestamp = {
lt: {},
gte: {seconds: 63393490800}
}];
- lt_now/gt_now: 这些不等式允许相对于当前时间的范围。这些规则不能与上面的绝对规则一起使用。
// x必须小于当前时间戳
google.protobuf.Timestamp x = 1 [(validate.rules).timestamp.lt_now = true];
- within: 此规则指定字段的值应在当前时间的一定持续时间内。此规则可与
lt_now
和gt_now
一起使用来控制这些范围。
// x必须在当前时间的±1秒内
google.protobuf.Timestamp x = 1 [(validate.rules).timestamp.within.seconds = 1];
// x必须在(now, now+1h)范围内
google.protobuf.Timestamp x = 1 [(validate.rules).timestamp = {
gt_now: true,
within: {seconds: 3600}
}];
消息全局
- disabled: 可以取消消息字段的所有验证规则,包括任何支持验证的消息字段本身。
message Person {
option (validate.disabled) = true;
// x将不需要大于123
uint64 x = 1 [(validate.rules).uint64.gt = 123];
// y的字段将不会被验证
Person y = 2;
}
- ignored: 不为此消息生成验证方法或任何相关的验证代码。
message Person {
option (validate.ignored) = true;
// x将不需要大于123
uint64 x = 1 [(validate.rules).uint64.gt = 123];
// y的字段将不会被验证
Person y = 2;
}
OneOfs
- required: 要求
oneof
中的一个字段必须被设置。默认情况下,联合字段中的无或一个可以被设置。启用此规则不允许全部未设置。
oneof id {
// x、y或z必须设置其中一个。
option (validate.required) = true;
string x = 1;
int32 y = 2;
Person z = 3;
}
开发
PGV是用Go语言在[protoc-gen-star][pg*]框架之上编写的,并编译成独立的二进制文件。
依赖
目前所有PGV依赖都已检入项目。要测试PGV,必须安装protoc
,可以从[源码][protoc-source]、提供的[发布版][protoc-releases]或包管理器安装。还应安装目标语言的官方protoc插件。
Make目标
-
make build
: 生成约束proto并将PGV编译到$GOPATH/bin
中 -
make lint
: 对PGV代码库运行静态分析规则,包括golint
、go vet
和gofmt -s
-
make testcases
: 生成/tests/harness/cases
中的proto文件。这些文件被测试工具用来验证为每种语言生成的验证规则。 -
make harness
: 对每种语言的测试工具执行测试用例。
在Bazel下运行所有测试
确保您的PATH
设置包含protoc-gen-go
和protoc
,然后:
bazel test //tests/...
Docker
PGV带有一个Dockerfile用于一致的开发工具和CI。主要入口点是make
,默认目标是build
。
# 构建镜像
docker build -t bufbuild/protoc-gen-validate .
# 执行默认make目标: build
docker run --rm \
bufbuild/protoc-gen-validate
# 执行'ci' make目标
docker run --rm \
bufbuild/protoc-gen-validate ci
# 执行'build'和'testcases' make目标
docker run --rm \
bufbuild/protoc-gen-validate build testcases
# 覆盖入口点并直接与容器交互
# 当想在本地没有安装bazel的情况下运行bazel命令时,这可能很有用。
docker run --rm \
-it --entrypoint=/bin/bash \
bufbuild/protoc-gen-validate