blog.syfm

徒然なるままに考えていることなどを書いていくブログ

より気軽に、簡単にテストができる gRPC クライアントをつくった

先日の A(izu)LT 0x09 で「Evans: more expressive universal gRPC client」 というタイトルで発表してきました。
タイトルの通り、gRPC クライアントをつくった話です 🙌

atnd.org

ALT は、会津大学の有志によって年に二回ほど開催されている LT (Lightning Talk) 会で、次回でついに 0x0a 回目を迎えます!めでたい 🎉

発表したスライドはアップロードしています。大体のことは以下に載っているけど、ちょっと補足をしておきたいと思います。

speakerdeck.com

つくったもの

github.com

Evans の目指したもの

Evans は既存の gRPC クライアントより簡単に扱えることを第一目標に作っています。
(gRPC に限らずですが、) gRPC を使っているプロダクト自体の品質を保ちたければ、gRPC を使ったテストも書いて CI で回せばいい話なので、gRPC クライアントはそれ以外のユースケースを補うためにあるべきだと思っています。

では補うべき部分とは一体なんなのかというと、

  • スクリプトによって何かのタスクを自動化したい
  • API を手動で検証したい

この二つだと思っています。
そして、この二つはそれぞれ以下の二つのモードと対応しています。

🖥️ コマンドラインモード

コマンドラインモードは、その名の通り一回のコマンドのみで完結するステートレスなモードで、既存の gRPC クライアントの Polyglot が採用している方式でもあります。

github.com

コマンドラインモードは、単なるコマンドなので、UNIX 哲学に則ってつくっています。
例えば、stdin から入力を受け取ることでフィルターとしての役割を果たすことができますし、その一方で出力は stdout へ JSON 形式で行われます。そのため、jq コマンドなどで適当に加工したりすることが可能です。

$ echo '{ "name": "ktr" }' | evans --package hello --service Greeter --call SayHello hello.proto | jq -r '.message'
# hello, ktr!

また、ファイルへ入力の JSON を保存して、何度も使いたい場合などのケースのために --file (-f) オプションを用意しており、こちらでも同じような動作ができます。

$ cat hello.json
{
  "name": "ktr"
}

$ evans -f hello.json --package hello --service Greeter --call SayHello hello.proto | jq -r '.message'
# hello, ktr!

ちなみに、コマンドラインモードでは事前に使用する package、service、RPC 名を知っている必要があるため、インタラクティブモードとは違い引数を省略できません。しかし、Git プロジェクトルートに .evans.toml がある場合は、デフォルトで使用するものを記述することができます。

[default]
package = "hello"
service = "Greeter"

こうすると以下のようにシンプルにコマンドを指定できます

$ evans -f hello.json --call SayHello hello.proto

🤼 インタラクティブモード

インタラクティブモードは、「API を手動で検証したい」時のためのモードです。
こちらの方は、なにも考えなくても使えるようにつくっています。
補完機能が使えるため、package 名や service 名、RPC 名を覚えていなくても大丈夫です!
このモードは MySQL のクライアントである、 dbcli/mycli を意識しています。

github.com

実際のデモはこんな感じです 👇

demo

読み込んでいる proto ファイル

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

enum Language {
  ENGLISH = 0;
  JAPANESE = 1;
}

message Person {
  string name = 1;
  repeated string others = 2;
}

message HelloRequest {
  Person person = 1;
  Language language = 2;
}

message HelloResponse {
  string message = 1;
}

サーバの実装

package main

import (
    "fmt"
    "log"
    "net"
    "strings"

    "golang.org/x/net/context"

    helloworld "github.com/ktr0731/evans/tmp"
    "google.golang.org/grpc"
)

type Greeter struct{}

func (t *Greeter) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloResponse, error) {
    msg := "Hello, %s also %s!"
    if req.GetLanguage() == helloworld.Language_JAPANESE {
        msg = "こんにちは、%s と %s !"
    }
    return &helloworld.HelloResponse{
        Message: fmt.Sprintf(msg, req.GetPerson().GetName(), strings.Join(req.GetPerson().GetOthers(), ", ")),
    }, nil
}

func main() {
    l, err := net.Listen("tcp", "localhost:50051")
    if err != nil {
        log.Fatal(err)
    }

    server := grpc.NewServer()
    helloworld.RegisterGreeterServer(server, &Greeter{})
    if err := server.Serve(l); err != nil {
        log.Fatal(err)
    }
}

べんりですね。

インストール

macOS / Linux 向けのバイナリが GitHub Releases にあるので、それを使えます。
もちろん、go get で取得することもできます。
macOS であれば、brew を使ってのインストールもできます 🍺

$ brew tap ktr0731/evans
$ brew install evans

Issue / PR

このツールは LT 駆動開発によって誕生したので、テストがありません 😱
なので、一旦新規の機能開発はやめてテストを充実させようと思っています 🙇‍♂️
その後、スライドでも言及したような TODO タスクを消化していきたいと思っています。
もし使っていただける方がいましたら、ぜひ Issue でも PR でもどんどんいただけたらありがたいです!