protovalidate触っていく
Protobufのスキーマをバリデーションできるprotoc-gen-validate(PGV)というライブラリがあった。
Envoy ProxyプロジェクトがJSONスキーマからProtobufへの移行を開始し、制約を定義できる方法としてPGVが選ばれ、2019年にEnvoyに開発が移行した。そして、2022年Bufにメンテナンスが引き継がれた。
PGVは十分に安定しており、既にその役割を終えたとして、後継のprotovalidateの開発が進んでいる。
詳しくはこちらのブログ原文を
protovalidateについて
protovalidateの大きな特徴としてGoogleが開発したCEL(Common Expression Language)を採用し、より柔軟な検証ルールを定義できるようにしている。protovalidateの主な目的はコードを生成することなくデータの一貫性と整合性を確保できるようにすることとしている。
CELについては頑張って調べたのでこちらをどうぞ
protovalidateのバージョンはまだ1.0.0に達していないがBufは新規および既存のプロジェクトはPGVの代わりにprotovalidateを使うことを推奨している。
執筆している時点で対応している言語はGo, C++, Java, Pythonでいずれもベータリリースである。TypeScriptは近日リリース予定とのこと。
protovalidateはPGVのすべての機能をサポートしている。
protovalidateの導入
mkdir proto
cd proto
buf mod init
mkdir -p example/hello/v1
touch example/hello/v1/hello.proto
tree .
.
├── buf.yaml
└── example
└── hello
└── v1
└── hello.proto
buf.yamlにprotovalidateの依存関係を追加します。
version: v1
+deps:
+ - buf.build/bufbuild/protovalidate
breaking:
use:
- FILE
lint:
use:
- DEFAULT
追加できたらbuf mod update
を実行してbuf.lock
ファイルを作成する。
buf mod update
tree .
.
├── buf.lock
├── buf.yaml
└── example
└── hello
└── v1
└── hello.proto
次にhello.protoの中身を以下のように実装します。
syntax = "proto3";
package example.hello.v1;
import "buf/validate/validate.proto";
message Hello { string hello = 1 [ (buf.validate.field).string.min_len = 1 ]; }
[ (buf.validate.field).string.min_len = 1 ]
の部分がprotovalidateの評価式で詳細は後述するが文字列の長さが1以上となるようなバリデーションルールです。
ちなみにVSCodeを使っていると以下のようなimportエラーがでるかもしれない。
これがでたらbufのVSCodeプラグインではなくvscode-proto3プラグイン起因でのエラーらしい。このエラーを消すにはBufのモジュールキャッシュをvscode-proto3プラグインのインポートに指定する。
"protoc": {
"options": ["-I=~/.cache/buf/v1/module/data/buf.build"]
}
protovalidate-go
上で作ったHello.protoのバリデーションを確認するためにprotovalidate-goの実装をして確認する。
go mod init protovalidate-demo
touch main.go
go get github.com/bufbuild/protovalidate-go
buf.gen.yamlを作成する。
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: protovalidate-demo/gen
disable:
- module: buf.build/bufbuild/protovalidate
file_option: go_package_prefix
plugins:
- remote: buf.build/protocolbuffers/go:v1.34.1
out: gen
opt: paths=source_relative
protovalidateと関係ないがBuf CLIのyamlに指定するバージョンが1から2になっていた。
なのでbuf.gen.yalはv2を指定して書いているためv1の書き方と少し違うけど内容は同じでより直感的にわかりやすく記述できるようになっている。
そういう書きづらさ、使いづらさを問題視してすぐさま改善するところがBufはすごいよね。本当にProtobufを使った開発体験について本気で向き合ってる感がすごい。
buf generate proto
生成されたhello.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: example/hello/v1/hello.proto
package hellov1
import (
_ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Hello struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Hello string `protobuf:"bytes,1,opt,name=hello,proto3" json:"hello,omitempty"`
}
func (x *Hello) Reset() {
*x = Hello{}
if protoimpl.UnsafeEnabled {
mi := &file_example_hello_v1_hello_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Hello) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Hello) ProtoMessage() {}
func (x *Hello) ProtoReflect() protoreflect.Message {
mi := &file_example_hello_v1_hello_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Hello.ProtoReflect.Descriptor instead.
func (*Hello) Descriptor() ([]byte, []int) {
return file_example_hello_v1_hello_proto_rawDescGZIP(), []int{0}
}
func (x *Hello) GetHello() string {
if x != nil {
return x.Hello
}
return ""
}
var File_example_hello_v1_hello_proto protoreflect.FileDescriptor
var file_example_hello_v1_hello_proto_rawDesc = []byte{
0x0a, 0x1c, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2f,
0x76, 0x31, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10,
0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x76, 0x31,
0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76,
0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x26, 0x0a,
0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x1d, 0x0a, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05,
0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x42, 0xb5, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78,
0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x76, 0x31, 0x42, 0x0a,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2d, 0x64, 0x65, 0x6d, 0x6f,
0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x68, 0x65, 0x6c,
0x6c, 0x6f, 0x2f, 0x76, 0x31, 0x3b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x76, 0x31, 0xa2, 0x02, 0x03,
0x45, 0x48, 0x58, 0xaa, 0x02, 0x10, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x48, 0x65,
0x6c, 0x6c, 0x6f, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
0x5c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x45, 0x78, 0x61, 0x6d,
0x70, 0x6c, 0x65, 0x5c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42,
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x12, 0x45, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x3a, 0x3a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_example_hello_v1_hello_proto_rawDescOnce sync.Once
file_example_hello_v1_hello_proto_rawDescData = file_example_hello_v1_hello_proto_rawDesc
)
func file_example_hello_v1_hello_proto_rawDescGZIP() []byte {
file_example_hello_v1_hello_proto_rawDescOnce.Do(func() {
file_example_hello_v1_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_hello_v1_hello_proto_rawDescData)
})
return file_example_hello_v1_hello_proto_rawDescData
}
var file_example_hello_v1_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_example_hello_v1_hello_proto_goTypes = []interface{}{
(*Hello)(nil), // 0: example.hello.v1.Hello
}
var file_example_hello_v1_hello_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_example_hello_v1_hello_proto_init() }
func file_example_hello_v1_hello_proto_init() {
if File_example_hello_v1_hello_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_example_hello_v1_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Hello); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_example_hello_v1_hello_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_example_hello_v1_hello_proto_goTypes,
DependencyIndexes: file_example_hello_v1_hello_proto_depIdxs,
MessageInfos: file_example_hello_v1_hello_proto_msgTypes,
}.Build()
File_example_hello_v1_hello_proto = out.File
file_example_hello_v1_hello_proto_rawDesc = nil
file_example_hello_v1_hello_proto_goTypes = nil
file_example_hello_v1_hello_proto_depIdxs = nil
}
package main
import (
"fmt"
hellov1 "protovalidate-demo/gen/hello/v1"
"github.com/bufbuild/protovalidate-go"
)
func main() {
msg := &hellov1.Hello{
Hello: "",
}
v, err := protovalidate.New()
if err != nil {
panic(err)
}
if err = v.Validate(msg); err != nil {
fmt.Println("validation failed:", err)
} else {
fmt.Println("validation succeeded")
}
}
go run main.go
validation failed: validation error:
- hello: value length must be at least 1 characters [string.min_len]
Helloは1文字以上の文字列を指定する必要があるためバリデーションエラーになっているため想定通りに動作している
connectrpc/validate-go
connectを使っている場合は以下のモジュールを使うことでconnectのinterceptorにバリデーションを設定することができる。
protovalidateにおけるCEL
protovalidateではCELの以下の機能を使用して制約を記述することができる。
- CELの組み込み関数
- マクロ
- CELのstring関数ライブラリ
- PGVの機能のためのカスタムの拡張関数
Conformance
あまり理解できなかったがprotovalidateのリポジトリ内のMakefileコマンドを実行して実行バイナリを得られ、これにバリデーションの期待する失敗テストをyamlに書いて渡してあげればテストスイートが実行できるよみたいな感じだろうか。
間違ってるかもしれないが雰囲気はそんな感じ
制約(Constraints)
protovalidateで言うところの制約は2種類ある。protovalidateで用意されている標準制約とCELで独自に記述するカスタム制約である。
カスタム制約ではフィールドレベルの制約とメッセージレベルの制約があるが、メッセージレベルの制約をCELを使いカスタム制約として書くことでMessage内のフィールド同士に跨った制約なども書くことができる。
Constraintメッセージはメッセージの検証に使用される。Constraintメッセージは以下のフィールドを持つ。
- id
- message 検証エラー時の人間が読めるエラー文
- expression CELによる検証式
実際の使用としては以下のようにProtobufのmessage内のoptionに指定したcelフィールドに上述のConstraintメッセージを指定する。
message Example{
option (buf.validate.message).cel = {
id: "message_expression_nested",
message: "a must be greater than b",
expression: "this.a > this.b"
};
int32 a = 1;
int32 b = 2;
}
フィールドレベルのバリデーションは以下のように指定する。
message Product {
double price = 1 [(buf.validate.field).cel = {
id: "product.price_range",
message: "Price must be between 0 and 1000",
expression: "this >= 0 && this <= 1000",
}];
}
Error
Validationメッセージは1つの制約違反を表し、これは以下の3つのフィールドを持つ。
- field_path バリデーションに失敗したフィールドへのパス
- constraint_id 制約のID
- message エラーメッセージ
Validationsメッセージは複数のValidationメッセージを持つ。
migrate
PGVからprotovalidateへのマイグレーションツールがちゃんと用意されてるよう
Standard constratins
上述したようにprotovalidateはCELで独自実装しなくても、標準的なバリデーションロジックをメッセージレベルやフィールドレベルで用意してくれている。
Message
messageで使用できる制約は執筆時点でdisabled
のみです。
message DisabledExample {
option (buf.validate.message).disabled = true;
// このバリデーションは無効化される
string val = 1 [ (buf.validate.field).string.min_len = 1 ];
}
oneof
oneofのoptionを拡張してprotovalidateでは制約が用意されている。執筆時点でoneofで使用できる制約は一つだけ。
message OneofExample {
oneof union {
option (buf.validate.oneof).required = true;
string val1 = 1;
string val2 = 2;
}
}
validation failed: validation error:
- union: exactly one field is required in oneof [required]
フィールド制約
google.protobuf.FieldOptions
を拡張してFieldConstraints
型の新しいオプションの名前付きフィールドを追加することで、Messageのフィールドに対して制約を記述することができる。
message Hello { string hello = 1 [ (buf.validate.field).string.min_len = 1 ]; }
string
string値に対してのバリデーション。
const
フィールドの値が指定の値と完全に一致しているか。
// const_value = "const" ok
// const_value = "const1" NG
string const_value = 1 [ (buf.validate.field).string.const = "const" ];
len
文字列の長さ
// len_value = "Hello!!" NG
// len_value = "Hello" OK
string len_value = 2 [ (buf.validate.field).string.len = 5 ];
min_len
文字列長の最小値
// min_len_value = "Hi" NG
string min_len_value = 3 [ (buf.validate.field).string.min_len = 5 ];
max_len
文字列長の最大値
// max_len_value = "Hello!!" NG
string max_len_value = 4 [ (buf.validate.field).string.max_len = 5 ];
len_bytes
文字列のバイト長
// 2バイトの文字列である必要がある
// len_bytes_value = "ab" OK
// len_bytes_value = "abc" NG
string len_bytes_value = 5 [ (buf.validate.field).string.len_bytes = 2 ];
min_bytes
最小のバイト長
// min_bytes_value = "a" NG
string min_bytes_value = 6 [ (buf.validate.field).string.min_bytes = 2 ];
max_bytes
最大のバイト長
// max_bytes_value = "abc" NG
string max_bytes_value = 7 [ (buf.validate.field).string.max_bytes = 2 ];
pattern
正規表現
// pattern_value = "hi, world" NG
// pattern_value = "hello, world" OK
string pattern_value = 8
[ (buf.validate.field).string.pattern = "^hello, .*$" ];
prefix
先頭文字マッチ
// prefix_value = "Hello, World" OK
string prefix_value = 9 [ (buf.validate.field).string.prefix = "Hello" ];
suffix
末尾マッチ
// suffix_value = "Hello, World" OK
// suffix_value = "Hello, Japan" NG
string suffix_value = 10 [ (buf.validate.field).string.suffix = "World" ];
contains
指定の文字列をふくむかどうか
// contains_value = "apple, banana, orange" OK
// contains_value = "apple, orange" NG
string contains_value = 11
[ (buf.validate.field).string.contains = "banana" ];
not_contains
指定の文字列を含んでいない
// not_contains_value = "apple, banana, orange" NG
// not_contains_value = "apple, orange" OK
string not_contains_value = 12
[ (buf.validate.field).string.not_contains = "banana" ];
in
指定の文字列listに対象の文字列が含まれるかどうか
// in_value = "Go" OK
// in_value = "Rust" NG
string in_value = 13 [
(buf.validate.field).string.in = "Java",
(buf.validate.field).string.in = "Kotlin",
(buf.validate.field).string.in = "Go"
];
not_in
指定の文字列listに対象の文字列が含まれないかどうか
// not_in_value = "Go" NG
// not_in_value = "Rust" OK
string not_in_value = 14 [
(buf.validate.field).string.not_in = "Java",
(buf.validate.field).string.not_in = "Kotlin",
(buf.validate.field).string.not_in = "Go"
];
// protovalidate@example.com OK
// protovalidate.example.com NG
string email_value = 15 [ (buf.validate.field).string.email = true ];
hostname
RFC1034で定義されているホスト名
これはprotovalidateで用意されているCEL関数のisHostname()
を使って検証している。
// 127.0.0.1 NG
// https://example.com NG
// example.com OK
string hostname_value = 16 [ (buf.validate.field).string.hostname = true ];
ip
ipv4 or ipv6
// 127.0.0.1 OK
// ::192.0.2.33 OK
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334 OK
// 255.255.255.256 NG
string ip_value = 17 [ (buf.validate.field).string.ip = true ];
ipv4
// 127.0.0.1 OK
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334 NG
string ipv4_value = 18 [ (buf.validate.field).string.ipv4 = true ];
ipv6
// 127.0.0.1 NG
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334 OK
string ipv6_value = 19 [ (buf.validate.field).string.ipv6 = true ];
uri
RFC3986で定義されているURI形式
protovalidateが用意しているCELのカスタム関数isURI()を使用して検証している
// https://example.com OK
// example.com NG
string uri_value = 20 [ (buf.validate.field).string.uri = true ];
uri_ref
相対パス含む
// ./example.com OK
string uri_ref_value = 21 [ (buf.validate.field).string.uri_ref = true ];
address
ホスト名もしくはIP
// 127.0.0.1 OK
// example.com OK
string address_value = 22 [ (buf.validate.field).string.address = true ];
uuid
RFC4122
// 550e8400-e29b-41d4-a716-446655440000 OK
string uuid_value = 23 [ (buf.validate.field).string.uuid = true ];
tuuid
RFC4122
// 550e8400e29b41d4a716446655440000 OK
string tuuid_value = 24 [ (buf.validate.field).string.tuuid = true ];
以下IP関連のバリデーションはBBSakuraさんがPRを出して追加されたようです。
ip_with_prefixlen
// 255.255.255.0/24 OK
// 255.255.255.0 NG
string ip_with_preifxlen_value = 25
[ (buf.validate.field).string.ip_with_prefixlen = true ];
ipv4_with_prefixlen
// 255.255.255.0/24 OK
// 255.255.255.0 NG
string ipv4_with_preifxlen_value = 26
[ (buf.validate.field).string.ipv4_with_prefixlen = true ];
ipv6_with_prefixlen
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334/24 OK
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334 NG
string ipv6_with_preifxlen_value = 27
[ (buf.validate.field).string.ipv6_with_prefixlen = true ];
ip_prefix
アドレス部分がネットワークアドレスになっていて、プレフィックス長が付いている値
ipv4もipv6も
// 127.0.0.0/16 OK
// 127.0.0.1/16 NG
string ip_prefix_value = 28 [ (buf.validate.field).string.ip_prefix = true ];
ipv4_prefix
// 127.0.0.0/16 OK
// 127.0.0.1/16 NG
string ip4_prefix_value = 29
[ (buf.validate.field).string.ipv4_prefix = true ];
ipv6_prefix
// 2001:db8::/48 OK
// 2001:db8::1/48 NG
string ip6_prefix_value = 30
[ (buf.validate.field).string.ipv6_prefix = true ];
host_and_port
有効なホスト名もしくはIPとportの組み合わせ
// 127.0.0.1:8080 OK
// 127.0.0.1 NG
// example.com:8080 OK
// example.com NG
// [::1]:1234 OK
string host_and_port_value = 31
[ (buf.validate.field).string.host_and_port = true ];
well_known_regex, strict
KnownRegexというenumを指定する。KownRegexは執筆時点で以下のような構造
enum KnownRegex {
KNOWN_REGEX_UNSPECIFIED = 0,
// RFC 7230で定義されているHTTPヘッダー名とその値の形式
KNOWN_REGEX_HTTP_HEADER_NAME = 1,
KNOWN_REGEX_HTTP_HEADER_VALUE = 2
}
// KnownRegex enumを指定する
// KNOWN_REGEX_HTTP_HEADER_NAME HTTPヘッダー名
// KNOWN_REGEX_HTTP_HEADER_VALUE HTTPヘッダー値
//
// Content-Type OK
// Content Type OK (strict = fasle)
// Content Type NG (strict = true)
string well_kown_regex_value = 32 [
(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_NAME,
(buf.validate.field).string.strict = false
];
strictを一緒に指定することでルールを緩めることお可能。デフォルトはtrue
Bool
bool値が指定の真偽値であることを検証する。
これはgoogle.protobuf.BoolValue
でも使用できる。
message BoolValidationExample {
// true_value = true OK
// true_value = false NG
bool true_value = 1 [ (buf.validate.field).bool.const = true ];
// false_value = true NG
// false_value = false OK
bool false_value = 2 [ (buf.validate.field).bool.const = false ];
}
Byte
byte値に対するバリデーション。これはgoogle.protobuf.BytesValue
でも使用できる。
const
指定のbyte値であることの検証
// 1234 OK
// 123 NG
bytes const_value = 1
[ (buf.validate.field).bytes.const = "\x01\x02\x03\x04" ];
len, min_len, max_len
// 1234 OK
// 123 NG
bytes len_value = 2 [ (buf.validate.field).bytes.len = 4 ];
// 123 OK
// 1 NG
bytes min_len_value = 3 [ (buf.validate.field).bytes.min_len = 2 ];
// 12 OK
// 123 NG
bytes max_len_value = 4 [ (buf.validate.field).bytes.max_len = 2 ];
pattern
byte値をUTF-8文字列に変換したときに指定の正規表現とマッチするかどうか
CELの評価式は!string(this).matches(rules.pattern)
となっている
// 0x61 (a) OK
// 0xe3, 0x81, 0x82 (あ) NG
bytes pattern_value = 5
[ (buf.validate.field).bytes.pattern = "^[a-zA-Z0-9]+$" ];
prefix, suffix
// 0x01, 0x02, 0x03 OK
// 0x01, 0x03, 0x02NG
bytes prefix_value = 6 [ (buf.validate.field).bytes.prefix = "\x01\x02" ];
// 0x01, 0x02, 0x03 OK
// 0x02, 0x01, 0x03 NG
bytes suffix_value = 7 [ (buf.validate.field).bytes.suffix = "\x02\x03" ];
contains
// 0x01, 0x02, 0x03 OK
// 0x01, 0x03 NG
bytes contains_value = 8 [ (buf.validate.field).bytes.contains = "\x02" ];
in, not_in
// 0x02, 0x03 OK
// 0x01, 0x02, 0x03 NG
bytes in_value = 9 [
(buf.validate.field).bytes.in = "\x01\x02",
(buf.validate.field).bytes.in = "\x02\x03",
(buf.validate.field).bytes.in = "\x03\x04"
];
// 0x02, 0x03 NG
// 0x01, 0x02, 0x03 OK
bytes not_in_value = 10 [
(buf.validate.field).bytes.not_in = "\x01\x02",
(buf.validate.field).bytes.not_in = "\x02\x03",
(buf.validate.field).bytes.not_in = "\x03\x04"
];
ip, ipv4, ipv6
ipv4は4バイト、ipv6は16バイト
// 0xFF, 0xFF, 0xFF, 0x00 (255.255.255.0) OK
// \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
// \xff\xff\xff\xff\xff\x00 (::ffff:ffff:ff00) OK
// \x01\x02 NG
bytes ip_value = 11 [ (buf.validate.field).bytes.ip = true ];
// 0xFF, 0xFF, 0xFF, 0x00 (255.255.255.0) OK
// \x01\x02 NG
bytes ipv4_value = 12 [ (buf.validate.field).bytes.ipv4 = true ];
// \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
// \xff\xff\xff\xff\xff\x00 (::ffff:ffff:ff00) OK
// \x01\x02 NG
bytes ipv6_value = 13 [ (buf.validate.field).bytes.ipv6 = true ];
Numeric constraints
- Double
- Float
- Fixed
- Fixed32
- SFixed32
- Fixed64
- SFixed64
- Int
- Int32
- Int64
- SInt32
- SInt64
const
// 42.0 OK
// 10.0 NG
double const_value = 1 [ (buf.validate.field).double.const = 42.0 ];
lt, lte
ltとlteは同時に指定することはできない
// 9.0 OK
// 10.0 NG
double lt_value = 2 [ (buf.validate.field).double.lt = 10.0 ];
// 10.0 OK
// 11.0 NG
double lte_value = 3 [ (buf.validate.field).double.lte = 10.0 ];
gt, gte
gtとgteは同時に適用することはできない
// 11.0 OK
// 10.0 NG
double gt_value = 4 [ (buf.validate.field).double.gt = 10.0 ];
// 10.0 OK
// 9.0 NG
double gte_value = 5 [ (buf.validate.field).double.gte = 10.0 ];
in, not_in
// 11.0 OK
// 13.0 NG
double in_value = 6 [
(buf.validate.field).double.in = 10.0,
(buf.validate.field).double.in = 11.0,
(buf.validate.field).double.in = 12.0
];
// 11.0 NG
// 13.0 OK
double not_in_value = 7 [
(buf.validate.field).double.not_in = 10.0,
(buf.validate.field).double.not_in = 11.0,
(buf.validate.field).double.not_in = 12.0
];
finite
double型の値のみ。
infiniteとNaNのときエラーとなる
// infinite or NaN NG double only
double finite_value = 8 [ (buf.validate.field).double.finite = true ];
enum
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
MY_ENUM_VALUE1 = 1;
MY_ENUM_VALUE2 = 2;
MY_ENUM_VALUE3 = 3;
}
const
// MY_ENUM_VALUE1 OK
// MY_ENUM_VALUE2 NG
MyEnum const_value = 1 [ (buf.validate.field).enum.const = 1 ];
defined_only
enumで定義されている値のみ
// Undefined Value 4 NG
MyEnum defined_only_value = 2
[ (buf.validate.field).enum.defined_only = true ];
in, not_in
// MY_ENUM_VALUE1 OK
// MY_ENUM_VALUE3 NG
MyEnum in_value = 3
[ (buf.validate.field).enum.in = 1, (buf.validate.field).enum.in = 2 ];
// MY_ENUM_VALUE1 NG
// MY_ENUM_VALUE3 OK
MyEnum not_in_value = 4 [
(buf.validate.field).enum.not_in = 1,
(buf.validate.field).enum.not_in = 2
];
map
min_pairs, max_pairs
最小、最大要素数
// {"key1": "value1", "key2": "value2"} OK
// {"key1": "value1"} NG
map<string, string> min_pairs_value = 1
[ (buf.validate.field).map.min_pairs = 2 ];
// {"key1": "value1", "key2": "value2"} OK
// {"key1": "value1", "key2": "value2", "key3": "value3"} NG
map<string, string> max_pairs_value = 2
[ (buf.validate.field).map.max_pairs = 2 ];
keys, values
key, valueそれぞれのデータ型に対しての制約
// {"a": "value1"} NG
// {"abcdefghijk": "value1"} NG
// {"key1": "value1"} OK
map<string, string> keys_value = 3
[ (buf.validate.field).map.keys = {string : {min_len : 3 max_len : 10}} ];
// {"key1": "a"} NG
// {"key1": "abcdefghijk"} NG
// {"key1": "value1"} OK
map<string, string> values_value = 4 [
(buf.validate.field).map.values = {string : {min_len : 3 max_len : 10}}
];
repeated
min_items, max_items
// ["elm1", "elm2"] OK
// ["elm1"] NG
repeated string min_items_value = 1
[ (buf.validate.field).repeated .min_items = 2 ];
// ["elm1", "elm2"] OK
// ["elm1", "el2", "el3"] NG
repeated string max_items_value = 2
[ (buf.validate.field).repeated .max_items = 2 ];
unique
// ["elm1", "elm2"] OK
// ["elm1", "elm2", "elm2"] NG
repeated string unique_value = 3
[ (buf.validate.field).repeated .unique = true ];
items
// ["a"] NG
// ["abcdefghijk"] NG
repeated string items_value = 4 [
(buf.validate.field).repeated .items = {string : {min_len : 3 max_len : 10}}
];
google.protobuf.Any
in, not_in
指定は型の完全修飾名で
// google.protobuf.Int32Value OK
// google.protobuf.BoolValue NG
google.protobuf.Any in_value = 1 [
(buf.validate.field).any.in =
"type.googleapis.com/google.protobuf.Int32Value",
(buf.validate.field).any.in =
"type.googleapis.com/google.protobuf.StringValue"
];
// google.protobuf.Int32Value NG
// google.protobuf.BoolValue OK
google.protobuf.Any not_in_value = 2 [
(buf.validate.field).any.not_in =
"type.googleapis.com/google.protobuf.Int32Value",
(buf.validate.field).any.not_in =
"type.googleapis.com/google.protobuf.StringValue"
];
google.protobuf.Duration
message Duration {
// Signed seconds of the span of time. Must be from -315,576,000,000
// to +315,576,000,000 inclusive. Note: these bounds are computed from:
// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
int64 seconds = 1;
// Signed fractions of a second at nanosecond resolution of the span
// of time. Durations less than one second are represented with a 0
// `seconds` field and a positive or negative `nanos` field. For durations
// of one second or more, a non-zero value for the `nanos` field must be
// of the same sign as the `seconds` field. Must be from -999,999,999
// to +999,999,999 inclusive.
int32 nanos = 2;
}
const
// <Go> durationpb.New(5 * time.Second) OK
google.protobuf.Duration const_value = 1
[ (buf.validate.field).duration.const = {seconds : 5} ];
lt, lte
// <Go> durationpb.New(4 * time.Second) OK
google.protobuf.Duration lt_value = 2
[ (buf.validate.field).duration.lt = {seconds : 5} ];
// <Go> durationpb.New(5 * time.Second) OK
google.protobuf.Duration lte_value = 3
[ (buf.validate.field).duration.lte = {seconds : 5} ];
gt, gte
// <Go> durationpb.New(6 * time.Second) OK
google.protobuf.Duration gt_value = 4
[ (buf.validate.field).duration.gt = {seconds : 5} ];
// <Go> durationpb.New(5 * time.Second) OK
google.protobuf.Duration gte_value = 5
[ (buf.validate.field).duration.gte = {seconds : 5} ];
in, not_in
// <Go> durationpb.New(5 * time.Second) OK
google.protobuf.Duration in_value = 6 [
(buf.validate.field).duration.in = {seconds : 5},
(buf.validate.field).duration.in = {seconds : 6},
(buf.validate.field).duration.in = {seconds : 7}
];
// <Go> durationpb.New(8 * time.Second) OK
google.protobuf.Duration not_in_value = 7 [
(buf.validate.field).duration.not_in = {seconds : 5},
(buf.validate.field).duration.not_in = {seconds : 6},
(buf.validate.field).duration.not_in = {seconds : 7}
];
google.protobuf.Timestamp
message Timestamp {
// Represents seconds of UTC time since Unix epoch
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
// 9999-12-31T23:59:59Z inclusive.
int64 seconds = 1;
// Non-negative fractions of a second at nanosecond resolution. Negative
// second values with fractions must still have non-negative nanos values
// that count forward in time. Must be from 0 to 999,999,999
// inclusive.
int32 nanos = 2;
}
const
// date -u -j -f "%Y-%m-%d %H:%M:%S" "2024-06-03 12:00:00" +%s
google.protobuf.Timestamp const_value = 1
[ (buf.validate.field).timestamp.const = {seconds : 1717416000} ];
lt, lte, lt_now
// date -j -f "%Y-%m-%d %H:%M:%S" "2024-06-03 11:00:00" +%s
// > 1717412400 OK
google.protobuf.Timestamp lt_value = 2
[ (buf.validate.field).timestamp.lt = {seconds : 1717416000} ];
// date -j -f "%Y-%m-%d %H:%M:%S" "2024-06-03 12:00:00" +%s
// > 1717416000 OK
google.protobuf.Timestamp lte_value = 3
[ (buf.validate.field).timestamp.lte = {seconds : 1717416000} ];
google.protobuf.Timestamp lt_now_value = 4
[ (buf.validate.field).timestamp.lt_now = true ];
gt, gte, gt_now
// date -j -f "%Y-%m-%d %H:%M:%S" "2024-06-03 13:00:00" +%s
// > 1717419600 OK
google.protobuf.Timestamp gt_value = 5
[ (buf.validate.field).timestamp.gt = {seconds : 1717416000} ];
// date -j -f "%Y-%m-%d %H:%M:%S" "2024-06-03 12:00:00" +%s
// > 1717416000 OK
google.protobuf.Timestamp gte_value = 6
[ (buf.validate.field).timestamp.gte = {seconds : 1717416000} ];
google.protobuf.Timestamp gt_now_value = 7
[ (buf.validate.field).timestamp.gt_now = true ];
within
現在時刻から指定のduration以内であること
// バリデーション時の現在時刻から前後1時間以内の時刻であること
google.protobuf.Timestamp within_value = 8
[ (buf.validate.field).timestamp.within = {seconds : 3600} ];
Other
今までに紹介してきたデータ型のルールはFieldConstraintsというmessageのフィールドの一部として定義されている。FieldConstraintsには他にも指定できるフィールドが存在する。
cel
CEL式をid, messageとともにフィールドの値に指定できる。
// 2 OK
// 3 NG
int32 even_value = 1 [ (buf.validate.field).cel = {
id : "int32.even",
message : "value must be even number",
expression : "this % 2 == 0",
} ];
required
適用するデータ型によって振る舞いが変わる
message MyValue { int32 value = 1; }
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_OK = 1;
}
// 値の指定がないとerror
MyValue required_message_value = 2 [ (buf.validate.field).required = true ];
// デフォルト値(空文字)だとエラー
string required_string_value = 3 [ (buf.validate.field).required = true ];
// デフォルト値(0)だとエラー
int32 required_int32_value = 4 [ (buf.validate.field).required = true ];
// 0がだめなのでenumの場合、未定義がエラーになる
Status required_enum_value = 5 [ (buf.validate.field).required = true ];
// 要素が0のときエラー
repeated string required_repeated_value = 6
[ (buf.validate.field).required = true ];
// 要素が0のときエラー
map<string, string> required_map_value = 7
[ (buf.validate.field).required = true ];
ignore
// 値が指定されていない時にはemail制約を無視する
string ignore_value = 8 [
(buf.validate.field).string.email = true,
(buf.validate.field).ignore = IGNORE_IF_UNPOPULATED
];
Conformance test
protovalidateで制約をつけたProtobufのテストに使うツールかと思っていたがこれはprotovalidateをサポートする各プログラミング言語ごとのライブラリの受け入れテストツールのよう。
protovalidate側で全ての制約を網羅したテストスイートを用意しており、それをTestConformanceRequest
として標準入力から受け取るようなExecuterと呼ばれる実行プログラムを用意し、Executer側でバリデーションを実行しテストがパスすることを確認するようだ。
protovalidate-goでは以下のようなExecuterを用意しているのでもし他の言語でprotovalidate対応ライブラリを開発する場合、同じようなExecuterを用意してテストする必要がある。
また、もしprotovalidateにPRを送り、新しい制約を追加してもらった場合、テストスイートも追加する必要がある。
ここら辺の話もBBSakuraさんのIPプレフィックスのPRの話が参考になる。