magicprotorb
Import .proto files directly in Ruby. No protoc, no generated _pb.rb files, no build step:
require "magicprotorb" # installs the import hook
require "magicprotorb/greet/hello_pb" # compiles greet/hello.proto
require "magicprotorb/greet/hello_services_pb" # + synthesizes the gRPC stub
req = Greet::HelloRequest.new(name: "world")
stub = Greet::Greeter::Stub.new("localhost:50051", :this_channel_is_insecure)
require "magicprotorb/greet/hello_pb" compiles greet/hello.proto (found on
MAGICPROTORB_PATH / $LOAD_PATH) at require time and defines the message
constants. The dotted require path mirrors the canonical proto path 1:1, so the
require name, the file location, and the descriptor name can never drift apart —
the classic "the generated import points at the wrong place" problem cannot occur.
How it works
A Kernel#require hook claims names under magicprotorb/ that end in _pb or
_services_pb (only).
magicprotorb/greet/hello_pb→ canonicalgreet/hello.proto, located on the include roots (theprotoc -Imodel:MAGICPROTORB_PATHthen$LOAD_PATH).- A small Rust extension (
magicprotorb_native, built on the pure-Rustprotoxcompiler) turns the.protointo a serializedFileDescriptorSet— the one thing the stock protobuf runtime can't do itself. - Those descriptors are registered through the stock
Google::Protobuf::DescriptorPool.generated_pool#add_serialized_file, and the message/enum constants are assigned exactly the way a generated_pb.rbdoes, so the message classes are indistinguishable from generated ones. _services_pbmodules are synthesized directly from the service descriptors as ordinaryGRPC::GenericServiceclasses (require "grpc"happens lazily).
See DESIGN.md for the full rationale, the multi-package namespacing model, and the limitations.
Where to put protos
Put foo/bar.proto where you'd want foo/bar.rb, and import it as
magicprotorb/foo/bar_pb.
A library ships its protos as data inside its own lib/ directory (already on
$LOAD_PATH); the directory name namespaces them, so two installed gems can't
collide.
Finding your protos (include roots)
magicprotorb resolves a proto by its canonical path against the include roots —
MAGICPROTORB_PATH first, then $LOAD_PATH — exactly like protoc -I. Note that
Ruby does not put the current directory on $LOAD_PATH, so a proto sitting
next to your script isn't found automatically. Make its directory an include root:
require "magicprotorb"
$LOAD_PATH.unshift __dir__ # this script's dir is now an include root
require "magicprotorb/keyvalue_pb" # resolves ./keyvalue.proto
or point MAGICPROTORB_PATH at it from the shell:
MAGICPROTORB_PATH="$PWD" ruby my_script.rb
Naming
| require | compiles | gives you |
|---|---|---|
magicprotorb/greet/hello_pb |
greet/hello.proto |
Greet::HelloRequest, ... |
magicprotorb/greet/hello_services_pb |
greet/hello.proto |
Greet::Greeter::Service / ::Stub |
The proto package becomes the Ruby module path the same way protoc's Ruby
generator does it: package my_co.sub_pkg.v1; → MyCo::SubPkg::V1.
There is also a programmatic API equivalent to the requires:
Magicprotorb.import("greet/hello") # like require "magicprotorb/greet/hello_pb"
Magicprotorb.import_services("greet/hello") # like require "magicprotorb/greet/hello_services_pb"
Magicprotorb.include_paths # the roots currently searched
Installation
# Gemfile
gem "magicprotorb"
Building the gem compiles the bundled Rust extension, so a Rust toolchain
(cargo) is required at install time. The runtime needs only google-protobuf
(and grpc, if you import services).
Installing from a local checkout
bundle exec rake install # builds the gem and installs it (native ext included)
After this, require "magicprotorb" works from any script without -I. The gem
installs into whichever Ruby is active (rbenv/rvm), so install under the same
Ruby you'll run with.
The
installtask deliberately runsgem installoutside the bundle (Bundler.with_unbundled_env). A native-extension gem whose own gemspec is the bundle's path gem otherwise fails to build at install time, because RubyGems' per-extension build dir goes missing underbundle exec.
Development
After checking out the repo:
bin/setup # install dependencies
bundle exec rake # compile the extension, then run the tests
bundle exec rake compile builds ext/magicprotorb_native into
lib/magicprotorb/. The fixture protos live in test/protos.