diff --git a/Rakefile b/Rakefile index 9d8afdd..eecad98 100644 --- a/Rakefile +++ b/Rakefile @@ -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") @@ -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" @@ -61,7 +66,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| diff --git a/lib/protoboeuf/autoloadergen.rb b/lib/protoboeuf/autoloadergen.rb new file mode 100644 index 0000000..49a8fad --- /dev/null +++ b/lib/protoboeuf/autoloadergen.rb @@ -0,0 +1,101 @@ +# 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_parts, + :require_paths_for_child_constants + + 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")].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 + + @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! + <%- 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 -%> + + <%- 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 diff --git a/lib/protoboeuf/google.rb b/lib/protoboeuf/google.rb index bd2ead5..0bbc9a3 100644 --- a/lib/protoboeuf/google.rb +++ b/lib/protoboeuf/google.rb @@ -2,4 +2,9 @@ # 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/gem_test.rb b/test/gem_test.rb index dd995dc..cad09c3 100644 --- a/test/gem_test.rb +++ b/test/gem_test.rb @@ -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