goのgRPCで便利ツールを使うで紹介されているGo gRPC MiddlewareとGolang ProtoBuf Validator CompilerでgRPCのvalidationをします。
今回の例では、User
の年齢は負数にならない、電話番号やメールアドレスを正規表現でvalidationするといったことを実装します。
インストール
Go gRPC Middlewareのインストール
❯ go get github.com/grpc-ecosystem/go-grpc-middleware
Golang ProtoBuf Validator Compilerのインストール
❯ go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
もとになるサーバとクライアント
protobufでgRPCを呼び出すサーバとクライアントを実装します。
今回はサーバにUser
を登録できるだけのサービスです。
protobufの定義
// user.proto syntax = "proto3"; package user; service UserService { rpc GetUser (Name) returns (User) {} rpc GetUsers (Empty) returns (Users) {} rpc AddUser (User) returns (Empty) {} } message User { string name = 1; int32 age = 2; string phone = 3; string mail = 4; } message Name { string name = 1; } message Empty {} message Users { repeated User users = 1; }
server
// server.go package main import ( "context" "errors" "log" "net" pb "github.com/cipepser/gRPC-validation/user" "google.golang.org/grpc" ) type server struct { users map[*pb.User]struct{} names map[string]struct{} } var ( empty = new(pb.Empty) ) const ( port = ":50051" ) func (s *server) GetUser(ctx context.Context, in *pb.Name) (*pb.User, error) { for u := range s.users { if u.Name == in.Name { return u, nil } } return nil, errors.New("user not found") } func (s *server) GetUsers(ctx context.Context, in *pb.Empty) (*pb.Users, error) { out := new(pb.Users) for u := range s.users { out.Users = append(out.Users, u) } return out, nil } func (s *server) AddUser(ctx context.Context, in *pb.User) (*pb.Empty, error) { if _, ok := s.names[in.Name]; ok { return empty, errors.New("user already exists") } s.users[in] = struct{}{} s.names[in.Name] = struct{}{} return empty, nil } func main() { l, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterUserServiceServer(s, &server{ users: map[*pb.User]struct{}{}, names: map[string]struct{}{}, }, ) s.Serve(l) }
client
// client.go package main import ( "context" "fmt" "log" pb "github.com/cipepser/gRPC-validation/user" "google.golang.org/grpc" ) const ( address = "localhost" port = ":50051" ) var ( empty = new(pb.Empty) ) type client struct { } func main() { conn, err := grpc.Dial(address+port, grpc.WithInsecure()) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close() c := pb.NewUserServiceClient(conn) u := pb.User{ Name: "Bob", Age: 24, Phone: "", Mail: "", } _, err = c.AddUser(context.Background(), &u) if err != nil { log.Fatalf("failed to add user: %v", err) } resp, err := c.GetUsers(context.Background(), empty) if err != nil { log.Fatalf("failed to get users: %v", err) } log.Printf("users:") for _, u := range resp.Users { fmt.Println(u) } }
validation
上記だけでもgo run server.go
でサーバを起動し、go run client.go
すればサービスが動きますが、以下に仕様に従ったvalidationをしてきましょう。
仕様
フィールド | 制約 |
---|---|
name | - |
age | 0〜150歳 |
phone | 携帯電話の正規表現にマッチ |
メールアドレスの正規表現にマッチ |
phone
とmail
の正規表現は、よく使う正規表現はもうググりたくない!から拝借します。そのままだと動かないので、エスケープを\
から\\
に変更しています。
Golang ProtoBuf Validator Compilerでは、
[(validator.field) = {msg_exists : true}];
とすることでrequired
を実現できますが、proto3でrequiredが廃止されたことからも利用しません(name
フィールドで使いたくなった)。
protobufの定義(validationあり)
// user.proto syntax = "proto3"; package user; import "github.com/mwitkow/go-proto-validators/validator.proto"; service UserService { rpc GetUser (Name) returns (User) {} rpc GetUsers (Empty) returns (Users) {} rpc AddUser (User) returns (Empty) {} } message User { string name = 1; int32 age = 2 [(validator.field) = {int_gt: -1, int_lt: 151}];; string phone = 3 [(validator.field) = {regex: "^(070|080|090)-\\d{4}-\\d{4}$"}]; string mail = 4 [(validator.field) = {regex: "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"}]; } message Name { string name = 1; } message Empty {} message Users { repeated User users = 1; }
以下でprotobufをコンパイルします。
❯ protoc \ --proto_path=${GOPATH}/src \ --proto_path=. \ --go_out=plugins=grpc:./ \ --govalidators_out=./ \ *.proto
server
Go gRPC Middlewareでvalidateさせるために、server.go
に以下を追記する。
// server.go
s := grpc.NewServer(
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_validator.StreamServerInterceptor(),
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_validator.UnaryServerInterceptor(),
)),
)
実行
validationされるか試してきましょう。
なお、ディレクトリ構成は以下のようになっています。
❯ tree . . ├── README.md ├── client │ └── client.go ├── server │ └── server.go └── user ├── user.pb.go ├── user.proto └── user.validator.pb.go 3 directories, 6 files
正常系
u := pb.User{ Name: "Alice", Age: 20, Phone: "090-1111-1111", Mail: "alice@example.com", }
❯ go run client/client.go 2018/07/22 14:20:28 users: name:"Alice" age:20 phone:"090-1111-1111" mail:"alice@example.com"
正常に登録できています。
異常系(age: -1歳)
u := pb.User{ Name: "Bob", Age: -1, Phone: "090-1111-1111", Mail: "bob@example.com", }
❯ go run client/client.go 2018/07/22 14:22:19 failed to add user: rpc error: code = InvalidArgument desc = invalid field Age: value '-1' must be greater than '-1' exit status 1
異常系(age: 200歳)
u := pb.User{ Name: "Bob", Age: 200, Phone: "090-1111-1111", Mail: "bob@example.com", }
❯ go run client/client.go 2018/07/22 14:22:35 failed to add user: rpc error: code = InvalidArgument desc = invalid field Age: value '200' must be less than '151' exit status 1
異常系(phone: 英字)
u := pb.User{ Name: "Bob", Age: 20, Phone: "09a-1111-11112", Mail: "bob@example.com", }
❯ go run client/client.go 2018/07/22 14:23:40 failed to add user: rpc error: code = InvalidArgument desc = invalid field Phone: value '09a-1111-1111' must be a string conforming to regex "^(070|080|090)-\\d{4}-\\d{4}$" exit status 1
異常系(phone: ハイフンなし)
u := pb.User{ Name: "Bob", Age: 20, Phone: "090111111112", Mail: "bob@example.com", }
❯ go run client/client.go 2018/07/22 14:23:48 failed to add user: rpc error: code = InvalidArgument desc = invalid field Phone: value '09011111111' must be a string conforming to regex "^(070|080|090)-\\d{4}-\\d{4}$" exit status 1
異常系(phone: 桁が多い)
u := pb.User{ Name: "Bob", Age: 20, Phone: "090-1111-11112", Mail: "bob@example.com", }
❯ go run client/client.go 2018/07/22 14:23:55 failed to add user: rpc error: code = InvalidArgument desc = invalid field Phone: value '090-1111-11112' must be a string conforming to regex "^(070|080|090)-\\d{4}-\\d{4}$" exit status 1
異常系(mail: @なし)
u := pb.User{ Name: "Bob", Age: 20, Phone: "090-1111-1111", Mail: "bob.example.com", }
❯ go run client/client.go 2018/07/22 14:24:40 failed to add user: rpc error: code = InvalidArgument desc = invalid field Mail: value 'bob.example.com' must be a string conforming to regex "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$" exit status 1
終わりに
異常系をちゃんとvalidationできていました。client側に正規表現の実装詳細を伝えてしまっているのが気になりますが、generateされたコードはuser/user.validator.pb.go
にあるので、そちらのメッセージを変更すれば見えなくなると思います。ただ、これでやるとgenerateし直すたびにuser/user.validator.pb.go
を直すことになるのでおすすめしませんが。