以前、Protocol Buffersのserviceの定義を利用してGoのnet/http
で利用できるようにするためのprotoc-gen-gohttpというprotocのプラグインを作成した。
しかし、protoc-gen-gohttpではURLのパス部分の定義はProtocol Buffersの定義には記述できないため、クライアント側がProtocol Buffersを見ただけでパスを読み取ることができない。 また、生成されたコードはHTTPのBodyしか参照しないため、情報を取得するだけのときもHTTPのメソッドをPOSTにする必要もあった。
そこで、Googleが提供しているRPCの定義をHTTPのREST APIにマッピングするためのHttpRuleオプションを利用して、protoc-gen-gohttpがHttpRuleのマッピングどおりに動作するコードを生成するように改良してみた。
使い方 Link to heading
protoc-gen-gohttpをインストールしたら、以下のようにHttpRuleを使用したProtoの定義を用意する(annotations.protoのダウンロードの仕方はこの辺を参考にしてください)。
この記事ではexample.proto
に書いているとする。
syntax = "proto3";
package main;
option go_package = "main";
import "google/api/annotations.proto";
service Messaging {
rpc GetMessage(GetMessageRequest) returns (GetMessageResponse) {
option (google.api.http).get = "/v1/messages/{message_id}";
}
rpc UpdateMessage(UpdateMessageRequest) returns (UpdateMessageResponse) {
option (google.api.http) = {
put: "/v1/messages/{message_id}/{sub.subfield}"
body: "*"
};
}
}
message GetMessageRequest {
string message_id = 1;
string message = 2;
repeated string tags = 3;
}
message GetMessageResponse {
string message_id = 1;
string message = 2;
repeated string tags = 4;
}
message SubMessage {
string subfield = 1;
}
message UpdateMessageRequest {
string message_id = 1;
SubMessage sub = 2;
string message = 3;
}
message UpdateMessageResponse {
string message_id = 1;
SubMessage sub = 2;
string message = 3;
}
用意できたら、以下のコマンドでGoのコードを生成する。
protoc --go_out=plugins=grpc:. --gohttp_out=. *.proto
生成されたコードを利用して、以下のように受け取ったものをただ返すだけのHTTPのWebサーバを実装する。
package main
import (
"context"
"log"
"net/http"
"github.com/go-chi/chi"
)
type Messaging struct{}
func (m *Messaging) GetMessage(ctx context.Context, req *GetMessageRequest) (*GetMessageResponse, error) {
return &GetMessageResponse{
MessageId: req.MessageId,
Message: req.Message,
Tags: req.Tags,
}, nil
}
func (m *Messaging) UpdateMessage(ctx context.Context, req *UpdateMessageRequest) (*UpdateMessageResponse, error) {
return &UpdateMessageResponse{
MessageId: req.MessageId,
Sub: &SubMessage{
Subfield: req.Sub.Subfield,
},
Message: "Hello World!",
}, nil
}
func main() {
conv := NewMessagingHTTPConverter(&Messaging{})
r := chi.NewRouter()
r.Method(conv.GetMessageHTTPRule(nil))
r.Method(conv.UpdateMessageHTTPRule(nil))
log.Fatal(http.ListenAndServe(":8080", r))
}
実装したサーバを起動したら、以下のようにAPIを叩くことで動作を確認できる。
curl -X GET -H 'Content-Type: application/json' 'localhost:8080/v1/messages/abc1234?message=hello&tags=a&tags=b'
curl -X PUT -H 'Content-Type: application/json' 'localhost:8080/v1/messages/abc1234/submsg' -d '{"messageId":"abc1234","sub":{"subfield":"submsg"},"message":"Hello World!"}'
簡単な解説 Link to heading
今まではprotoc-gen-gohttpはGetMessage
メソッドやGetMessageWithName
メソッドのような2つのパターンしか生成しなかった。
今回の修正で、HttpRuleオプションをRPCに記述していたらGetMessageHTTPRule
というメソッドも生成するようになった。
HTTPRule
という接尾語がついたメソッドは、Protocol Buffersに定義されているHttpRuleのオプションのうち、メソッド名(string)、パス名(string)及びhttp.HandlerFuncを返すようになっている。
GetMessageHTTPRule
の場合、返り値は"GET"
、"/v1/messages/{message_id}"
及びhttp.HandlerFunc
を返す。
また、GetMessageはGET
メソッドをOptionで指定されているため、http.HandlerFunc
はQuery Stringを解析してGetMessageRequest
に情報を詰めるようになっている。
パスパラメータにも対応しているため、{message_id}
のように{}
で囲った部分はmessageの定義へマッピングされるようになっている。
今後やりたいこと Link to heading
Query String対応でコードがだいぶ荒れたので、まずはリファクタリングをしたい。
また、HttpRuleオプションのうちadditional_bindingsへの対応ができてなかったり、Bodyに*
以外を指定してもマッピングをしてくれなかったりとHttpRuleに定義されている仕様からだいぶ漏れているものがあるのでそれらの対応もしていきたいなぁと思う。