diff --git a/.gitattributes b/.gitattributes index f71bfbb..2831bbd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore index d97aaab..609dbb4 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Rakefile b/Rakefile index 9d8afdd..283d26d 100644 --- a/Rakefile +++ b/Rakefile @@ -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" @@ -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 } @@ -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, @@ -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| @@ -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 diff --git a/lib/protoboeuf/autoloadergen.rb b/lib/protoboeuf/autoloadergen.rb new file mode 100644 index 0000000..7d1196e --- /dev/null +++ b/lib/protoboeuf/autoloadergen.rb @@ -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 diff --git a/lib/protoboeuf/google.rb b/lib/protoboeuf/google.rb index bd2ead5..7e931cb 100644 --- a/lib/protoboeuf/google.rb +++ b/lib/protoboeuf/google.rb @@ -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 diff --git a/lib/protoboeuf/google/api.rb b/lib/protoboeuf/google/api.rb new file mode 100644 index 0000000..1b88d5a --- /dev/null +++ b/lib/protoboeuf/google/api.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +# rubocop:disable all + +# Autogenerated by `rake well_known_types`. Do not edit! +module ProtoBoeuf + module Google + module Api + autoload :FieldBehavior, "protoboeuf/google/api/field_behavior" + end + end +end diff --git a/lib/protoboeuf/google/protobuf.rb b/lib/protoboeuf/google/protobuf.rb new file mode 100644 index 0000000..f267c67 --- /dev/null +++ b/lib/protoboeuf/google/protobuf.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +# rubocop:disable all + +# Autogenerated by `rake well_known_types`. Do not edit! +module ProtoBoeuf + module Google + module Protobuf + autoload :Any, "protoboeuf/google/protobuf/any" + autoload :BoolValue, "protoboeuf/google/protobuf/boolvalue" + autoload :BytesValue, "protoboeuf/google/protobuf/bytesvalue" + autoload :DescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :DoubleValue, "protoboeuf/google/protobuf/doublevalue" + autoload :Duration, "protoboeuf/google/protobuf/duration" + autoload :Edition, "protoboeuf/google/protobuf/descriptor" + autoload :EnumDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :EnumOptions, "protoboeuf/google/protobuf/descriptor" + autoload :EnumValueDescriptorProto, + "protoboeuf/google/protobuf/descriptor" + autoload :EnumValueOptions, "protoboeuf/google/protobuf/descriptor" + autoload :ExtensionRangeOptions, "protoboeuf/google/protobuf/descriptor" + autoload :FeatureSet, "protoboeuf/google/protobuf/descriptor" + autoload :FeatureSetDefaults, "protoboeuf/google/protobuf/descriptor" + autoload :FieldDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :FieldMask, "protoboeuf/google/protobuf/field_mask" + autoload :FieldOptions, "protoboeuf/google/protobuf/descriptor" + autoload :FileDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :FileDescriptorSet, "protoboeuf/google/protobuf/descriptor" + autoload :FileOptions, "protoboeuf/google/protobuf/descriptor" + autoload :FloatValue, "protoboeuf/google/protobuf/floatvalue" + autoload :GeneratedCodeInfo, "protoboeuf/google/protobuf/descriptor" + autoload :Int32Value, "protoboeuf/google/protobuf/int32value" + autoload :Int64Value, "protoboeuf/google/protobuf/int64value" + autoload :ListValue, "protoboeuf/google/protobuf/struct" + autoload :MessageOptions, "protoboeuf/google/protobuf/descriptor" + autoload :MethodDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :MethodOptions, "protoboeuf/google/protobuf/descriptor" + autoload :NullValue, "protoboeuf/google/protobuf/struct" + autoload :OneofDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :OneofOptions, "protoboeuf/google/protobuf/descriptor" + autoload :ServiceDescriptorProto, "protoboeuf/google/protobuf/descriptor" + autoload :ServiceOptions, "protoboeuf/google/protobuf/descriptor" + autoload :SourceCodeInfo, "protoboeuf/google/protobuf/descriptor" + autoload :StringValue, "protoboeuf/google/protobuf/stringvalue" + autoload :Struct, "protoboeuf/google/protobuf/struct" + autoload :Timestamp, "protoboeuf/google/protobuf/timestamp" + autoload :UInt32Value, "protoboeuf/google/protobuf/uint32value" + autoload :UInt64Value, "protoboeuf/google/protobuf/uint64value" + autoload :UninterpretedOption, "protoboeuf/google/protobuf/descriptor" + autoload :Value, "protoboeuf/google/protobuf/struct" + end + end +end diff --git a/test/autoloadergen_test.rb b/test/autoloadergen_test.rb new file mode 100644 index 0000000..7a9b955 --- /dev/null +++ b/test/autoloadergen_test.rb @@ -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 diff --git a/test/fixtures/autoloadergen/google/test_protos.correct.rb b/test/fixtures/autoloadergen/google/test_protos.correct.rb new file mode 100644 index 0000000..f26ca7d --- /dev/null +++ b/test/fixtures/autoloadergen/google/test_protos.correct.rb @@ -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 diff --git a/test/fixtures/autoloadergen/google/test_protos/color.proto b/test/fixtures/autoloadergen/google/test_protos/color.proto new file mode 100644 index 0000000..3358a2d --- /dev/null +++ b/test/fixtures/autoloadergen/google/test_protos/color.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package google.test_protos; + +enum Color { + UNKNOWN = 0; + RED = 1; + BLUE = 2; + GREEN = 3; +} diff --git a/test/fixtures/autoloadergen/google/test_protos/transportation.proto b/test/fixtures/autoloadergen/google/test_protos/transportation.proto new file mode 100644 index 0000000..c870af3 --- /dev/null +++ b/test/fixtures/autoloadergen/google/test_protos/transportation.proto @@ -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; +} diff --git a/test/gem_test.rb b/test/gem_test.rb index dd995dc..b7c4ef9 100644 --- a/test/gem_test.rb +++ b/test/gem_test.rb @@ -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