Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QOL make our gem requireable and autoload/eager load some constants #171

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 :BarMessage1, "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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This String#pathmap comes from rake.
Can we simplify that to get rid of that dependency?
Maybe just Dir["#{module_filename.delete_suffix(".rb")}/**/*.rb"] would be enough?

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_full_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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever get set?

autoloader_full_module_name = child_constants.first.split("::")[0..2].join("::")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we generalize this 2 in case there's ever a different level of nesting?
What do we really want here, everything but the last one?
or one more than whatever is in parent_module?

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("::")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use this value for something else, or are we joining it just to split it again?
Is there a better way to store this?

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
Loading