grpc_required_annotator gem つくった
TL;DR
- 会社で grpc ruby を使っていて、同じような実装何回もやってんなと思ったので共通化して gem にした
- request message を required チェックを簡単にできる DSL を提供してくれる
- いつも書いていた required をチェックするだけの冗長なテストがすごく短くなって生産性上がった
本編
最近会社で grpc ruby を使った backend を書いていて、「同じような実装何回もやってんな」と思うことがあった。
それは request message の required な field に対して、空だったときに GRPC::InvalidArgument
を返すやつ。
class SampleService < SamplePb::Sample::Service def foo(request, call): raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0 # これ raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields) end end
これに対応するテストを書くのもめんどくさい。
describe SampleService do describe "#foo" do subject { described_class.new.foo(request) } context "when num is empty" do let(:request) { SamplePb::Sample::FooRequest.new( num: nil, str: "str", repeated_fields: ["a"] ) } it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end context "when str is empty" do ... it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end context "when repeated_fields is empty" do ... it "raises GRPC::InvalidArgument" do expect { subject }.to raise_error(GRPC::InvalidArgument) end end end end
shared_exaple を使えばそれなりに共通化はできるけど request のクラスは各 rpc ごとで異なるので微妙に使いにくい。
というわけでシュッと required な field を宣言できて、パッとテストを書けるいい感じの gem を作ろうと思った。
できたのがこちら
この gem に入っている GrpcRequiredAnnotator
module を service の実装クラスに include することで required
メソッドが使えるようになる。
class SampleService < SamplePb::Sample::Service include GrpcRequiredAnnotator required :num, :str, :repeated_fields def foo(request, call): # raise GRPC::InvalidArgument.new("`num` is required") if req.num == 0 # これ # raise GRPC::InvalidArgument.new("`str` is required") if req.str == "" # これも # raise GRPC::InvalidArgument.new("`repeated_fields` is required") if req.repeated_fields.empty? # 嫌になってくる FooResponse.new(num: req.num, str: req.str, repeated_fields: req.repeated_fields) end end
こんな感じで各 rpc に対応するメソッドのすぐ上で required にしたい field を symbol で宣言すると実行前に validation してくれるようになる。便利。
各 rpc に対応する required な field を取得できるメソッドも生やしているのでテストもかなり行数が減った。
RSpec.describe SampleService do describe "#foo" do describe "required" do it "num, str and repeated_fields are required" do expect(described_class.required_fields(:foo)).to eq [:num, :str, :repeated_fields] end end end end
結構便利だと思うので grpc ruby 書いてる人はぜひ使ってみてください。
method_added
で added された method の挙動を override して再定義してたりとか、その再定義を module の extend で実現しようとしたら grpc interceptor の治安の悪い仕様1によって壊れてしまった話は元気があればまた書きます。