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 001c6c2
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 3 deletions.
31 changes: 29 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ require "rubocop/rake_task"
RuboCop::RakeTask.new

BASE_DIR = File.dirname(__FILE__)
codegen_rb_files = ["lib/protoboeuf/codegen.rb"]
LIB_DIR = File.expand_path(BASE_DIR, "lib")

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

Expand All @@ -19,14 +21,17 @@ 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) }
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|
rule ".rb" => ["%X.proto"] + codegen_rb_files do |t| # codegen_rb_files = ["lib/protoboeuf/codegen.rb"]
codegen_rb_files.each { |f| require_relative f }

require "tempfile"
Expand Down Expand Up @@ -106,6 +111,28 @@ end
desc "Regenerate protobuf files"
task gen_proto: rb_files

desc "Regenerate autoloader modules for well-known types"
task well_known_autoloaders: [:well_known_types] + codegen_rb_files do
# 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 :BarConst1, "proto_boeuf/google/protobuf/bar"
# end
# end
# end

require_relative "lib/protoboeuf/autoloadergen"

WELL_KNOWN_AUTOLOADERS.each do |autoloader_rb_file|
File.binwrite(autoloader_rb_file, ProtoBoeuf::AutoloaderGen.new(autoloader_rb_file).to_ruby)
end
end

task test: [:gen_proto, :well_known_types]
task default: :test

Expand Down
95 changes: 95 additions & 0 deletions lib/protoboeuf/autoloadergen.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 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,
:autoloader_module_name,
:autoloader_module_parts,
:child_constants_by_filename

def initialize(module_filename)
@module_filename = module_filename
# Given lib/protoboeuf/google.rb, glob lib/protoboeuf/google/**/*.rb
@child_ruby_filenames = Dir[module_filename.pathmap("%X/**/*.rb")]
@autoloader_module_name = nil
@child_constants_by_filename = child_ruby_filenames.each_with_object({}) do |filename, constants|
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.
@autoloader_module_name = child_constants.first.split("::")[0..2].join("::") if @autoloader_module_name.nil?

# 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("")
constants[require_path] = child_constants
end

@autoloader_module_parts = @autoloader_module_name.split("::")
end

def to_ruby
SyntaxTree.format(ERB.new(<<~RUBY, trim_mode: "-").result(binding))
# frozen_string_literal: true
# rubocop:disable all
<%- autoloader_module_parts.each do |module_name| -%>
module <%= module_name %>
<%- end -%>
<%- child_constants_by_filename.each do |require_path, constant_names| -%>
<%- constant_names.each do |constant_name| -%>
autoload :<%= constant_name.split("::").last %>, "<%= require_path %>"
<%- end -%>
<%- end -%>
<%- 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::ProtoBoeuf::Google.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
8 changes: 7 additions & 1 deletion lib/protoboeuf/google.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@

# 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 }
# 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
12 changes: 12 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.

53 changes: 53 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.

2 changes: 2 additions & 0 deletions test/gem_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def test_can_be_required
::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 001c6c2

Please sign in to comment.