見出し画像

【Go】gRPCのリクエストバリデータを自動生成する

こんにちは🐔
Showcase Gigでプラットフォーム開発1のエンジニアリングマネージャーをしている林 (howyi) です。

現在開発しているプロダクトでは主にGolang+gRPCのサーバを作成しています。この記事では、grpc-goを用いたGolangのgRPCサーバの構築時のリクエスト検証方法の1つを紹介します。

対象環境

grpc-goをつかったgRPCサーバ

バリデーション機能の導入

APIサーバを実装していると、「リクエストの mail_address はメールアドレスとして解釈可能な文字列しか許可しない」といった、リクエストのパラメータに細かい制限を入れたいと言った要件はよく出てきます。

このような要件に対応する場合、grpc-goでは公式で grpc_validator というミドルウェアが用意されています。

import (
	grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
)
~~~~~~~~~~~
func (s *server) Start() {
~~~~~~~~~~~
   g := grpc.NewServer(
		grpc.UnaryInterceptor(
			grpc_middleware.ChainUnaryServer(
				grpc_validator.UnaryServerInterceptor(),
			),
		),
		grpc.StreamInterceptor(
			grpc_middleware.ChainStreamServer(
				grpc_validator.StreamServerInterceptor(),
			),
		),
	)
~~~~~~~~~~~
}

​このInterceptorを導入すると、リクエストされたstructに Validate メソッドが用意されていた場合のみメソッドを実行し、処理前に値の検証(バリデーション)がされます。

Validate メソッドの管理

リクエストされるstructに手動で Validate を記入し、各チェックを記載することでも実現可能です。

ただ、あまりにも手間が大きいので、今回はこの Validate メソッドをprotoファイルから自動生成できる protoc-gen-validate というprotocプラグインを紹介します。

protoc-gen-validate

protoc-gen-validate は、envoy が開発、公開しているprotoc pluginです。
protoに定義された独自記法のバリデータから、Golangを含むいくつかの言語のサーバ向けのバリデーションコードを自動生成できます。
※現在α版で、API変更される可能性があると明記されています。

Golangで同様の事が可能なライブラリはほかにもありますが、生成されるGolangのコード単体で見てもこのプラグインが個人的に一番使いやすいと感じました。

他のライブラリと比較したメリット

・設定可能な項目の多さ
・エラーメッセージの親切さ
・α版ではありますが、envoy管理のOSSという信頼

使い方

1. protoファイルにバリデーションを記述する

github.com/envoyproxy/protoc-gen-validate/validate/validate.proto をimportし、validate.rulesを記入できるようにしたうえで、各フィールドにルールを記述します。

設定できる値が多いためすべては記載していないので、 READMEのconstraint-rules を参照してください。

server.proto

import "github.com/envoyproxy/protoc-gen-validate/validate/validate.proto";
~~~~~~~~~~~
service TestServer {
   rpc Test(TestMessage) returns (Result) {}
}
~~~~~~~~~~~
message TestMessage {
   // 0~100間の整数
   int32 seisuu = 1 [(validate.rules).int32 = {gte:0, lt: 100}];
   // floatで0~1の値
   double fudou = 2 [(validate.rules).double = {gte: 0, lte: 1}];
   // アルファベットと数値で、5〜30文字のrepeated
   repeated string mojiretsu = 3 [(validate.rules).repeated.items.string = {pattern: "^[a-z0-9]{5,30}$", min_len: 5, max_len:30}];
   // RFC 1034で解釈可能なメールアドレス
   string mail_address = 4 [(validate.rules).string.email = true];
}

2. protocコマンドでgolangのコードを生成する

protocコマンドで --validate_out を指定したうえで自動生成します。

protoc \
 -I . \
 -I ${GOPATH}/src \
 -I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
 --go_out=":../generated" \
 --validate_out="lang=go:../generated" \
 server.proto

実行後、このようなメソッドが生成され、TestMessage のメソッドとなっていれば成功です。

生成されたコードが人間にも読みやすいのは助かりますね。

server.pb.validate.go

func (m *TestMessage) Validate() error {
	if m == nil {
		return nil
	}

	if val := m.GetSeisuu(); val < 0 || val >= 100 {
		return TestMessageValidationError{
			field:  "Seisuu",
			reason: "value must be inside range [0, 100)",
		}
	}

	if val := m.GetFudou(); val < 0 || val > 1 {
		return TestMessageValidationError{
			field:  "Fudou",
			reason: "value must be inside range [0, 1]",
		}
	}

	for idx, item := range m.GetMojiretsu() {
		_, _ = idx, item

		if l := utf8.RuneCountInString(item); l < 5 || l > 30 {
			return TestMessageValidationError{
				field:  fmt.Sprintf("Mojiretsu[%v]", idx),
				reason: "value length must be between 5 and 30 runes, inclusive",
			}
		}

		if !_TestMessage_Mojiretsu_Pattern.MatchString(item) {
			return TestMessageValidationError{
				field:  fmt.Sprintf("Mojiretsu[%v]", idx),
				reason: "value does not match regex pattern \"^[a-z0-9]{5,30}$\"",
			}
		}

	}

	if err := m._validateEmail(m.GetMailAddress()); err != nil {
		return TestMessageValidationError{
			field:  "MailAddress",
			reason: "value must be a valid email address",
			cause:  err,
		}
	}

	return nil
}

実行

正常

まずは正常なリクエストを発行します。

{
 "seisuu": 10,
 "fudou": 0.4,
 "mojiretsu": [
   "testtest",
   "testtest"
 ],
 "mail_address": "test@example.com"
}

Validate が通り、正しく結果が返ってきました。

{
 "result": true
}

文字数のバリデーションエラー

次はmojiretsu(制限: 各要素5~10文字)を、一部4文字にして送ってみます。

{
 "seisuu": 10,
 "fudou": 0.4,
 "mojiretsu": [
   "testtest",
   "test",
   "testtest"
 ],
 "mail_address": "test@example.com"
}

エラーが出ました。
TestMessage.Mojiretsu[1] というように、配列内のどこで出たかも記載されていますね。

{
 "error": "3 INVALID_ARGUMENT: invalid TestMessage.Mojiretsu[1]: value length must be between 5 and 30 runes, inclusive"
}

メールアドレスのバリデーションエラー

次はメールアドレスのフォーマットを間違ってみます。

{
 "seisuu": 10,
 "fudou": 0.4,
 "mojiretsu": [
   "testtest",
   "testtest"
 ],
 "mail_address": "test"
}

メールアドレスのバリデーションで失敗したエラーメッセージが出てきます。

{
 "error": "3 INVALID_ARGUMENT: invalid TestMessage.MailAddress: value must be a valid email address | caused by: mail: missing '@' or angle-addr"
}

数ヵ所でのバリデーションエラー

次は、複数の箇所で間違ってみます。

{
 "seisuu": 10,
 "fudou": 1.4,
 "mojiretsu": [
   "testtest",
   "test",
   "testtest"
 ],
 "mail_address": "test"
}

エラーは返ってきましたが、一番上で出たバリデーションエラーのみ返ってきています

{
 "error": "3 INVALID_ARGUMENT: invalid TestMessage.Fudou: value must be inside range [0, 1]"
}

生成されたソースコードをみるとわかりますが、上の項目からチェック処理を行い、弾かれた時点でエラーをreturnしている事が原因です。

この問題については issue が上がっており、現在DraftでPRが上がっている状態です。

今後に期待です 👀

そのほかの問題点

定義がproto2で書かれている

コード生成がproto3のみ対応しているprotocのプラグインなどもあります。
例えばphpのクライアントコードを自動生成する時はgrpc_php_pluginを使う必要がありますが、このプラグインはproto3のコードしか許可されていないため、 validate.go を生成対象とした場合は以下のようなエラーが発生します。

--php_out: protoc-gen-validate/validate/validate.proto: Can only generate PHP code for proto3 .proto files.
Please add 'syntax = "proto3";' to the top of your .proto file.

これについては、生成対象から外したうえでproto3の validate.go をダミーで作成する、という対処があります。

issueが上がっており、今後grpc_php_plugin側にproto2対応が入る、という予定があるとのことです。

しかし、起票から2年経った今も未対応のため、対応される望みは薄そうです。 validator自体はクライアントコードに機能を持たないため、ダミーを作成する形で対処する事自体は問題ありません。

まとめ

バリデーションエラー発生時、エラー内容がまとまって出てこないという問題はあるものの、バリデーションエラー自体がレアケースになるはずであることを考えると、全体としてかなり実用的かつ便利なライブラリです。

本記事が、golangでgRPCサーバを実装する際の助けとなれば幸いです。

宣伝

Showcase Gigでは多種多様な技術に挑戦する仲間を募集しています!
ぜひチェックしてみてください ✨


この記事が気に入ったらサポートをしてみませんか?