以前、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に定義されている仕様からだいぶ漏れているものがあるのでそれらの対応もしていきたいなぁと思う。