やりたいこと
- railsでzeitwerkを有効にしつつProtocolBuffersを利用したい
前提
Protocol Buffersについての説明はしません
普通に使ってみる
# Gemfile
gem 'google-protobuf'
// proto/teacher.proto
syntax = "proto3";
message Teacher {
string id = 1;
string name = 2;
}
.proto
からrubyのソースコードを生成する
protoc --proto_path=protos --ruby_out=app/pb/ protos/*.proto
を実行すると、 app/pb/teacher_pb.rb
に以下のようなファイルが生成されます。
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: teacher.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("teacher.proto", :syntax => :proto3) do
add_message "Teacher" do
optional :id, :string, 1
optional :name, :string, 2
end
end
end
Teacher = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Teacher").msgclass
これをそのまま使おうとすると失敗します。
% bin/rails c
Loading development environment (Rails 7.0.2.2)
irb(main):001:0> Teacher
(irb):1:in `<main>': uninitialized constant Teacher (NameError)
Did you mean? TeacherPb
irb(main):002:0> TeacherPb
/home/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/zeitwerk-2.5.4/lib/zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded': expected file /home/development/rails-playground/app/pb/teacher_pb.rb to define constant TeacherPb, but didn't (Zeitwerk::NameError)
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
^^^^^
irb(main):003:0> Pb::Teacher
(irb):3:in `<main>': uninitialized constant Pb (NameError)
zeitwerk環境でも読み込めるようにする
理由や対応方法などはRails6でProtocol Buffersを使用するがわかりやすいです。記事に習ってinflectorを追加してみましょう。
-- 記事から抜粋 --
class WithProtobufInflector < Zeitwerk::Inflector
def camelize(basename, abspath)
if basename =~ /\A.*_pb$/
basename.gsub("_pb", '').camelize
else
super
end
end
end
Rails.autoloaders.each do |autoloader|
autoloader.inflector = WithProtobufInflector.new
end
-- 記事から抜粋(終わり) --
うまくいきました。
% bin/rails c
Loading development environment (Rails 7.0.2.2)
irb(main):001:0> Teacher.new(name: 'foo')
=> <Teacher: id: "", name: "foo">
これをcontrollerに組み込んでも普通に使えます。
class WelcomeController < ApplicationController
def index
@teacher = Teacher.new(name: 'foo')
end
end
しかし、controllerを変更するなどしてrails再読み込みが発生するとUnable to build file to DescriptorPool: duplicate file name (teacher.proto)
というエラーが発生する
再読み込みを可能にする
これはファイルの変更時にzeitwerkのreloadingが走っていることが原因です。これをを防ぐためにはprotobuf部分を外部gem化すると良いです。 zeitwerkのドキュメントにあるように、一般的にGemはreloadingをする必要がなく、zeitwerkのデフォルトの設定でもreloadingは無効になっています。
Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading.
gemの作成
bundle gem k_ota_schema_registry
他の言語からも使われることを想定して、ruby独自の部分はサブディレクトリに移しておく
cd k_ota_schema_registry
mkdir -p protos/k_ota_schema_registry ruby
(rubyのgemのdirectory構造に合わせて出力できるようにprotoを置くディレクトリを作っておく)
mv bin lib Gemfile k_ota_schema_registry.gemspec Rakefile ruby
protocコマンドは対象となるprotoファイルを一つずつ指定する必要があるので、rakeタスクで一括生成できるようにしておく
# ruby/Rakefile
require "bundler/gem_tasks"
task default: 'protobuf:generate'
namespace :protobuf do
desc 'generate ruby file'
task :generate do
protofiles = Dir.glob('protos/**/*.proto', base: '..')
system(['protoc --proto_path=protos --ruby_out=ruby/lib', *protofiles].join(' '), chdir: '..')
end
end
また、zeitwerk環境でも読み込めるようにしたように、zeitwerk対応を入れる。READMEの構造に、先程のzeitwerk対応のコードを入れれば良い。
# ruby/lib/k_ota_schema_registry.rb
require "zeitwerk"
require_relative "k_ota_schema_registry/inflector"
loader = Zeitwerk::Loader.for_gem
loader.inflector = KOtaSchemaRegistry::Inflector.new(__FILE__)
loader.setup
module KOtaSchemaRegistry
class Error < StandardError; end
# Your code goes here...
end
# ruby/lib/k_ota_schema_registry/inflector.rb
module KOtaSchemaRegistry
class Inflector < Zeitwerk::GemInflector
def camelize(basename, abspath)
if basename =~ /\A.*_pb$/
basename.gsub("_pb", '').camelize
else
super
end
end
end
end
利用する側は素直にgemfileとして使える
# Gemfile
gem 'k_ota_schema_registry', git: 'https://github.com/k-o-ta/k_ota_schema_registry.git', branch: 'main'
% bin/rails c
Loading development environment (Rails 7.0.2.2)
irb(main):002:0> KOtaSchemaRegistry::Teacher.new(id: '1', name: 'foo')
=> <KOtaSchemaRegistry::Teacher: id: "1", name: "foo">
irb(main):003:0> KOtaSchemaRegistry::Foo::Student.new(id: '1', name: 'bar')
=> <KOtaSchemaRegistry::Foo::Student: id: "1", name: "bar">
irb(main):004:0> reload!
Reloading...
=> true
# reloadしても問題ない
irb(main):005:0> KOtaSchemaRegistry::Foo::Student.new(id: '1', name: 'bar')
=> <KOtaSchemaRegistry::Foo::Student: id: "1", name: "bar">