男女比はカレーと福神漬けと同じくらい

マサカリよろしくお願いします。

grpc_required_annotator gem つくった

rubygems.org

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 を作ろうと思った。
できたのがこちら

github.com

この 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によって壊れてしまった話は元気があればまた書きます。


  1. grpc interceptor は intercept した method の method インスタンスを参照できる