2024-11-20
スキーマ駆動CLIツール開発を支援するツールをprotoに乗っかって作りたい
以下のようなprotoスキーマを定義する。
syntax = "proto3";
package nfurudono.sample.v1;
service SamplService {
rpc Say(SayRequest) returns (SayResponse) {}
}
// SayRequest is a single-sentence request.
message SayRequest {
string sentence = 1;
}
// SayResponse is a single-sentence response.
message SayResponse {
string sentence = 1;
bool dry_run = 2;
}
こんな定義があるときに
```sh
$ ./sample say --sentence "hello world" --dryrun
sentence: "I will say: hello world"
みたいなやりとりを定義するためのCLIテンプレート生成ツールを作る。あくまでテンプレートなので、生成ツールではインターフェースとかグルーコードだけ提供して、ユーザは以下のようなコードを書くことになる。
- connectとかでサービスとサーバの起動を定義してmainからいい感じに呼ぶようにすると、gRPCサーバがたつのに対して、
- CLIテンプレート作成ツールでサービスとCLIコマンドの呼び出しを定義してmainからいい感じに呼ぶようにすると、CLIから実行できる
サービスの実装
package service
improt (
"fmt"
sample "github.com/nfurudono/gen/go/sample"
)
type SampleImpl struct {}
// sample.SampleServiceはprotocとかが生成するようなinterface。gRPCとかconnectとかで使われているようなやつ。
var _ sample.SampleService = &NewSampleImpl()
func NewSampleImpl() { return SampleImple{} }
func (* SampleImpl) Say(ctx *context.Context, req *connect.Request[samplev1.SayRequest]) (*connect.Response[samplev1.Response], error) {
s := req.GetSentence()
d := req.GetDryRun()
if d {
return fmt.Sprintf("I will say: %s", s), nil
}
return fmt.Sprintf("%s!!", s), nil
}
エントリポイント
package main
import (
slog
tool "github.com/naoyafurudono/good-tool"
"github.com/naoyafurudono/sample-cli/service"
)
func main() {
s := service.NewSampleImpl()
// NewCLI()の結果(CLI)はサービスを登録される。
// サービスを保持するCLIはサービスが契約するrpc(名前、入力、出力)を知っている。
// これらはprotoの仕組みで生成される。protovalidateなどのプラグインもそのレイヤで対応できるはず。
cli := tool.NewCLI().AddService(s) // このあたりのインターフェースはもうちょい考えても良いかも?
// Runがコマンドライン引数を読んで以下を半ドアリングする
// - 呼び出すrpcの判定(ルーティング)、サブコマンドの名前が対応する
// - rpcに渡す入力messageのデコード、サブコマンドへのフラグ引数が対応する
// - rpcの出力メッセージやエラー内容の出力、コマンドの標準出力、標準エラー出力、コマンドのステータスコードの出しわけが対応する
if err := cli.Run(); err != nil {
slog.Fatalf("unexpected inpu: %w", err) // 予期しないサブコマンドが来たらエラーを返すのもまた一興かな。
}
}
便利じゃない?