Skip to content

Commit

Permalink
ProtoBoeuf::AutoloaderGen will generate helper modules to autoload ou…
Browse files Browse the repository at this point in the history
…r generated constants
  • Loading branch information
davebenvenuti committed Jan 17, 2025
1 parent ecdc7b5 commit a4ea5b8
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
lib/protoboeuf/google/**/*.rb linguist-generated=true
test/fixtures/autoloadergen/google/**/*.rb linguist-generated=true
test/fixtures/autoloadergen/google/test_protos.correct.rb linguist-generated=false
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ Gemfile.lock
# Code generated by protoboeuf
test/fixtures/*_pb.rb
test/fixtures/typed_test.generated.rb
test/fixtures/autoloadergen/google/**/*.rb
!test/fixtures/autoloadergen/google/test_protos.correct.rb
bench/lib/
bench/tmp/
47 changes: 40 additions & 7 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ require "rubocop/rake_task"
RuboCop::RakeTask.new

BASE_DIR = File.dirname(__FILE__)
codegen_rb_files = ["lib/protoboeuf/codegen.rb"]
proto_files = Rake::FileList[File.join(BASE_DIR, "test/fixtures/*.proto")]
rb_files = proto_files.pathmap("#{BASE_DIR}/test/fixtures/%n_pb.rb")
LIB_DIR = File.expand_path(BASE_DIR, "lib")

codegen_rb_files = ["lib/protoboeuf/codegen.rb", "lib/protoboeuf/autoloadergen.rb"]

# Fixture protos we want to compile with protoc
protoc_test_fixtures = Rake::FileList[File.join(BASE_DIR, "test/fixtures/*.proto")]
protoc_test_fixtures_rb_files = protoc_test_fixtures.pathmap("#{BASE_DIR}/test/fixtures/%n_pb.rb")

# Fixture protos we want to compile with protoboeuf
protoboeuf_test_fixtures = Rake::FileList[File.join(BASE_DIR, "test/fixtures/autoloadergen/**/*.proto")]
protoboeuf_test_fixtures_rb_files = protoboeuf_test_fixtures.pathmap("%X.rb")

BENCHMARK_UPSTREAM_PB = "bench/lib/upstream/benchmark_pb.rb"
BENCHMARK_PROTOBOEUF_PB = "bench/lib/protoboeuf/benchmark_pb.rb"
Expand All @@ -19,12 +27,16 @@ well_known_types = Rake::FileList[
]

WELL_KNOWN_PB = well_known_types.pathmap("%X.rb")
# For a directory like lib/protoboeuf/google/protobuf, create an autoloader in lib/protoboeuf/google/protobuf.rb
WELL_KNOWN_AUTOLOADERS = well_known_types.pathmap("%d.rb").uniq

# Clobber/clean rules
rb_files.each { |x| CLOBBER.append(x) }
protoc_test_fixtures_rb_files.each { |x| CLOBBER.append(x) }
protoboeuf_test_fixtures_rb_files.each { |x| CLOBBER.append(x) }
CLOBBER.append(BENCHMARK_UPSTREAM_PB)
CLOBBER.append(BENCHMARK_PROTOBOEUF_PB)
CLOBBER.append(WELL_KNOWN_PB)
CLOBBER.append(WELL_KNOWN_AUTOLOADERS)

rule ".rb" => ["%X.proto"] + codegen_rb_files do |t|
codegen_rb_files.each { |f| require_relative f }
Expand All @@ -34,9 +46,10 @@ rule ".rb" => ["%X.proto"] + codegen_rb_files do |t|

unit = Tempfile.create(File.basename(t.source)) do |f|
File.unlink(f.path)

sh(*[
"protoc",
well_known_types.map { |file| ["-I", file.pathmap("%d")] }.uniq,
(well_known_types + [t.source]).map { |file| ["-I", file.pathmap("%d")] }.uniq,
File.basename(t.source),
"-o",
f.path,
Expand All @@ -61,7 +74,27 @@ rule ".rb" => ["%X.proto"] + codegen_rb_files do |t|
File.binwrite(t.name, ProtoBoeuf::CodeGen.new(unit).to_ruby(dest, options))
end

task well_known_types: WELL_KNOWN_PB
rule ".rb" => "%X" do |t|
# Given lib/protoboeuf/google/protobuf/foo.rb and lib/protoboeuf/google/protobuf/bar.rb, generate
# lib/protoboeuf/google/protobuf.rb that looks like:
#
# module ProtoBoeuf
# module Google
# module Protobuf
# autoload :FooMessage1, "proto_boeuf/google/protobuf/foo"
# autoload :FooMessage2, "proto_boeuf/google/protobuf/foo"
# autoload :BarMessage1, "proto_boeuf/google/protobuf/bar"
# end
# end
# end

require_relative "lib/protoboeuf/autoloadergen"

puts "writing autoloader module #{t.name}"
File.binwrite(t.name, ProtoBoeuf::AutoloaderGen.new(t.name).to_ruby)
end

task well_known_types: WELL_KNOWN_PB + WELL_KNOWN_AUTOLOADERS

# Makefile-like rule to generate "_pb.rb"
rule "_pb.rb" => "test/fixtures/%{_pb,}n.proto" do |task|
Expand Down Expand Up @@ -104,7 +137,7 @@ Rake::TestTask.new do |t|
end

desc "Regenerate protobuf files"
task gen_proto: rb_files
task gen_proto: protoc_test_fixtures_rb_files + protoboeuf_test_fixtures_rb_files

task test: [:gen_proto, :well_known_types]
task default: :test
Expand Down
112 changes: 112 additions & 0 deletions lib/protoboeuf/autoloadergen.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# frozen_string_literal: true

require "erb"
require "syntax_tree"
require "pathname"

module ProtoBoeuf
class AutoloaderGen
# This class generates top-level autoloader modules for our well known types. Given autogenerated .rb files like:
# - lib/protoboeuf/google/protobuf/foo.rb
# - lib/protoboeuf/google/protobuf/bar.rb
#
# generate lib/protoboeuf/google/protobuf.rb that looks like:
#
# module ProtoBoeuf
# module Google
# module Protobuf
# autoload :FooMessage1, "protoboeuf/google/protobuf/foo"
# autoload :FooMessage2, "protoboeuf/google/protobuf/foo"
# autoload :BarConst1, "protoboeuf/google/protobuf/bar"
# end
# end
# end

BASE_LIB_DIR = File.expand_path("..", __dir__)

attr_reader :module_filename,
:child_ruby_filenames,
:generated_autoloader_module_parts,
:parent_module_parts,
:require_paths_for_child_constants

def initialize(module_filename, parent_module = "ProtoBoeuf::Google")
@module_filename = module_filename
@parent_module_parts = parent_module.split("::")

# Given lib/protoboeuf/google.rb, glob lib/protoboeuf/google/**/*.rb
@child_ruby_filenames = Dir[module_filename.pathmap("%X/**/*.rb")].sort
autoloader_full_module_name = nil

# Build a map of what we want to autoload :ConstantName => protoboeuf/require/path
@require_paths_for_child_constants = child_ruby_filenames.each_with_object({}) do |filename, require_paths|
child_constants = constants_for_child_ruby_filename(filename)
# For the autoloader_module_name we can just pick the first child constant we come across and take the first
# three parts. For example, ProtoBoeuf::Google::Api::FieldBehavior would be ProtoBoeuf::Google::Api.
if @autoloader_module_name.nil?
autoloader_full_module_name = child_constants.first.split("::")[0..2].join("::")
end

# Make our absolute filename relative to the base lib directory for our autoload calls.
require_path = Pathname.new(filename).relative_path_from(BASE_LIB_DIR).sub_ext("")
child_constants.each do |child_constant|
# child_constant is fully qualified, but we just want the last part
require_paths[child_constant.split("::").last] = require_path
end
end

@generated_autoloader_module_parts = autoloader_full_module_name.split("::")
end

def to_ruby
SyntaxTree.format(ERB.new(<<~RUBY, trim_mode: "-").result(binding))
# frozen_string_literal: true
# rubocop:disable all
# Autogenerated by `rake well_known_types`. Do not edit!
<%- generated_autoloader_module_parts.each do |module_name| -%>
module <%= module_name %>
<%- end -%>
<%-
# Iterating over the sorted keys gives us lexographically sorted autoload statements
-%>
<%- require_paths_for_child_constants.keys.sort.each do |constant_name| -%>
<%- require_path = require_paths_for_child_constants[constant_name] -%>
autoload :<%= constant_name %>, "<%= require_path %>"
<%- end -%>
<%- generated_autoloader_module_parts.each do |module_name| -%>
end
<%- end -%>
RUBY
end

private

def constants_for_child_ruby_filename(filename)
@constants_for_child_ruby_filename ||= {}

return @constants_for_child_ruby_filename[filename] if @constants_for_child_ruby_filename.key?(filename)

loaded = Module.new do
module_eval File.binread(filename)
end

@constants_for_child_ruby_filename[filename] = loaded::ProtoBoeuf::Google.constants.flat_map do |const_name|
mod = loaded
parent_module_parts.each do |part|
mod = mod.const_get(part)
end
mod = mod.const_get(const_name)

next unless mod.is_a?(Module)

# The top-level module will be our anonymous Module we created above
parent_module_name = mod.name.split("::")[1..].join("::")

mod.constants.map { |const_name| "#{parent_module_name}::#{const_name}" }
end
end
end
end
9 changes: 6 additions & 3 deletions lib/protoboeuf/google.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

# There isn't a clean 1:1 mapping between constants and *.rb files, so eager load instead of autoload.

Dir[File.expand_path("google/**/*.rb", __dir__)].each { |file| require file }
module ProtoBoeuf
module Google
autoload :Api, "protoboeuf/google/api"
autoload :Protobuf, "protoboeuf/google/protobuf"
end
end
11 changes: 11 additions & 0 deletions lib/protoboeuf/google/api.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions lib/protoboeuf/google/protobuf.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions test/autoloadergen_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require "helper"
require "protoboeuf/autoloadergen"

class AutoloaderGenTest < ProtoBoeuf::Test
FIXTURE_PATH = File.expand_path("fixtures/autoloadergen/google", __dir__)

def test_generates_autoloader_module
# test/fixtures/autoloadergen/google/test_protos/*.proto needs an autoloader at
# test/fixtures/autoloadergen/google/test_protos.rb
autoloader_rb_path = File.expand_path("test_protos.rb", FIXTURE_PATH)

autoloader_ruby = ProtoBoeuf::AutoloaderGen.new(autoloader_rb_path).to_ruby

# If you ever want to regenerate the expected_autoloader_ruby, run:
# File.binwrite(File.expand_path("test_protos.correct.rb", FIXTURE_PATH), autoloader_ruby)
expected_autoloader_ruby = File.binread(File.expand_path("test_protos.correct.rb", FIXTURE_PATH))

assert_equal(expected_autoloader_ruby, autoloader_ruby)
end
end
17 changes: 17 additions & 0 deletions test/fixtures/autoloadergen/google/test_protos.correct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true
# rubocop:disable all

# Autogenerated by `rake well_known_types`. Do not edit!
module ProtoBoeuf
module Google
module TestProtos
autoload :Bicycle,
"../test/fixtures/autoloadergen/google/test_protos/transportation"
autoload :Boat,
"../test/fixtures/autoloadergen/google/test_protos/transportation"
autoload :Color, "../test/fixtures/autoloadergen/google/test_protos/color"
autoload :Vehicle,
"../test/fixtures/autoloadergen/google/test_protos/transportation"
end
end
end
10 changes: 10 additions & 0 deletions test/fixtures/autoloadergen/google/test_protos/color.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
syntax = "proto3";

package google.test_protos;

enum Color {
UNKNOWN = 0;
RED = 1;
BLUE = 2;
GREEN = 3;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
syntax = "proto3";

package google.test_protos;

import "color.proto";

message Boat {
string make = 1;
int32 year = 2;
Color color = 3;
}

message Bicycle {
string make = 1;
int32 year = 2;
Color color = 3;
}

message Vehicle {
string make = 1;
string model = 2;
int32 year = 3;
Color color = 4;
}
4 changes: 3 additions & 1 deletion test/gem_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ def test_can_be_required
::ProtoBoeuf
# The following should auto/eagerload
# The following should autoloaded
::ProtoBoeuf::CodeGen
::ProtoBoeuf::Google::Api::FieldBehavior
::ProtoBoeuf::Google::Protobuf::Any
::ProtoBoeuf::Google::Protobuf::FileDescriptorProto
::ProtoBoeuf::Google::Protobuf::FileDescriptorSet
exit 0
RUBY
Expand Down

0 comments on commit a4ea5b8

Please sign in to comment.