diff --git a/lib/protobuf/protoc/generator.ex b/lib/protobuf/protoc/generator.ex index 16f7f464..421aa812 100644 --- a/lib/protobuf/protoc/generator.ex +++ b/lib/protobuf/protoc/generator.ex @@ -8,6 +8,10 @@ defmodule Protobuf.Protoc.Generator do @spec generate(Context.t(), %Google.Protobuf.FileDescriptorProto{}) :: {term(), [Google.Protobuf.Compiler.CodeGeneratorResponse.File.t()]} def generate(%Context{} = ctx, %Google.Protobuf.FileDescriptorProto{} = desc) do + ctx = + %{ctx | package: desc.package} + |> Protobuf.Protoc.Context.custom_file_options_from_file_desc(desc) + {package_level_extensions, module_definitions} = generate_module_definitions(ctx, desc) files = @@ -17,8 +21,12 @@ defmodule Protobuf.Protoc.Generator do %Google.Protobuf.Compiler.CodeGeneratorResponse.File{name: file_name, content: content} end) else - # desc.name is the filename, ending in ".proto". - file_name = Path.rootname(desc.name) <> ".pb.ex" + base_module = Generator.Util.mod_name(ctx, []) + file_name = + case base_module do + "" -> Path.rootname(desc.name) <> ".pb.ex" + _ -> Macro.underscore(base_module) <> "/" <> Path.rootname(desc.name) <> ".pb.ex" + end content = module_definitions diff --git a/test/protobuf/protoc/cli_integration_test.exs b/test/protobuf/protoc/cli_integration_test.exs index 4e559736..16d59a9f 100644 --- a/test/protobuf/protoc/cli_integration_test.exs +++ b/test/protobuf/protoc/cli_integration_test.exs @@ -30,7 +30,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/foo/user.pb.ex") assert mod == Foo.User end @@ -43,7 +43,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/foo/user.pb.ex") assert mod == Foo.User assert mod.transform_module() == TestMsg.TransformModule @@ -58,7 +58,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/foo/user.pb.ex") assert mod == Foo.User assert %Google.Protobuf.DescriptorProto{} = descriptor = mod.descriptor() @@ -74,7 +74,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - modules_and_docs = get_docs_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + modules_and_docs = get_docs_and_clean_modules_on_exit("#{tmp_dir}/foo/user.pb.ex") assert [{Foo.User, docs}] = modules_and_docs assert {:docs_v1, _, :elixir, _, module_doc, _, _} = docs assert module_doc != :hidden @@ -88,7 +88,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - modules_and_docs = get_docs_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + modules_and_docs = get_docs_and_clean_modules_on_exit("#{tmp_dir}/foo/user.pb.ex") assert [{Foo.User, docs}] = modules_and_docs assert {:docs_v1, _, :elixir, _, :hidden, _, _} = docs @@ -122,7 +122,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/user.pb.ex") + assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/mypkg/foo/user.pb.ex") assert mod == Mypkg.Foo.User end @@ -178,7 +178,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path ]) - assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/timestamp_wrapper.pb.ex") + assert [mod] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/my_type/timestamp_wrapper.pb.ex") assert mod == MyType.TimestampWrapper assert Map.fetch!(mod.__message_props__().field_props, 1).type == Google.Protobuf.Timestamp @@ -244,13 +244,13 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do ]) assert [Bugs.Base = base_mod] = - compile_file_and_clean_modules_on_exit("#{tmp_dir}/base.pb.ex") + compile_file_and_clean_modules_on_exit("#{tmp_dir}/bugs/base.pb.ex") assert [Bugs.Ext1, Bugs.Ext1.PbExtension] = - compile_file_and_clean_modules_on_exit("#{tmp_dir}/ext1.pb.ex") + compile_file_and_clean_modules_on_exit("#{tmp_dir}/bugs/ext1.pb.ex") assert [Bugs.Ext2, Bugs.Ext2.PbExtension] = - compile_file_and_clean_modules_on_exit("#{tmp_dir}/ext2.pb.ex") + compile_file_and_clean_modules_on_exit("#{tmp_dir}/bugs/ext2.pb.ex") assert [Bugs.PbExtension] = compile_file_and_clean_modules_on_exit("#{tmp_dir}/bugs/pb_extension.pb.ex") @@ -343,8 +343,8 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do proto_path_two ]) - elixir_one = File.read!("#{tmp_dir}/one.pb.ex") - elixir_two = File.read!("#{tmp_dir}/two.pb.ex") + elixir_one = File.read!("#{tmp_dir}/tests/one.pb.ex") + elixir_two = File.read!("#{tmp_dir}/tests/two.pb.ex") assert elixir_one =~ """ defmodule Tests.One do @@ -414,7 +414,7 @@ defmodule Protobuf.Protoc.CLIIntegrationTest do ]) assert [message_mod, extension_mod] = - compile_file_and_clean_modules_on_exit("#{tmp_dir}/extensions.pb.ex") + compile_file_and_clean_modules_on_exit("#{tmp_dir}/my_pkg/extensions.pb.ex") assert %Google.Protobuf.MessageOptions{} = options = message_mod.descriptor().options assert options.__pb_extensions__ == %{{extension_mod, :notes} => "This messge is cool"} diff --git a/test/protobuf/protoc/generator/file_path_test.exs b/test/protobuf/protoc/generator/file_path_test.exs new file mode 100644 index 00000000..37058ba9 --- /dev/null +++ b/test/protobuf/protoc/generator/file_path_test.exs @@ -0,0 +1,93 @@ +defmodule Protobuf.Protoc.FilePathTest do + use ExUnit.Case, async: true + + alias Google.Protobuf.{DescriptorProto, FileDescriptorProto} + alias Protobuf.Protoc.Context + alias Protobuf.Protoc.Generator + + describe "generate/2 - file naming with module_prefix" do + test "file path does not respect module_prefix when one_file_per_module is false (default)" do + ctx = %Context{ + module_prefix: "MyApp.V1", + package: nil, + syntax: :proto2, + one_file_per_module?: false, + dep_type_mapping: %{}, + global_type_mapping: %{"example.proto" => %{}} + } + + desc = %FileDescriptorProto{ + name: "example.proto", + message_type: [ + %DescriptorProto{ + name: "Foo" + } + ] + } + + {_package_level_exts, files} = Generator.generate(ctx, desc) + + assert length(files) == 1 + file = List.first(files) + + assert file.content =~ "defmodule MyApp.V1.Foo do" + + assert file.name == "my_app/v1/example.pb.ex", + "File path should respect module_prefix. Expected 'my_app/v1/example.pb.ex' to match module 'MyApp.V1.Foo', but got '#{file.name}'" + end + + test "file path respects module_prefix when one_file_per_module is true" do + ctx = %Context{ + module_prefix: "MyApp.V1", + package: nil, + syntax: :proto2, + one_file_per_module?: true, + dep_type_mapping: %{}, + global_type_mapping: %{"example.proto" => %{}} + } + + desc = %FileDescriptorProto{ + name: "example.proto", + message_type: [ + %DescriptorProto{ + name: "Foo" + } + ] + } + + {_package_level_exts, files} = Generator.generate(ctx, desc) + + assert length(files) == 1 + file = List.first(files) + + assert file.name == "my_app/v1/foo.pb.ex", + "Expected 'my_app/v1/foo.pb.ex' but got '#{file.name}'" + end + + test "file path should respect module_prefix with extensions" do + ctx = %Context{ + module_prefix: "Protobuf.Protoc.ExtTest", + package: "ext", + syntax: :proto2, + one_file_per_module?: false, + dep_type_mapping: %{".ext.Foo" => %{type_name: "Protobuf.Protoc.ExtTest.Foo"}}, + global_type_mapping: %{"extension.proto" => %{}} + } + + desc = %FileDescriptorProto{ + name: "extension.proto", + message_type: [ + %DescriptorProto{name: "Foo"} + ] + } + + {_package_level_exts, files} = Generator.generate(ctx, desc) + + assert length(files) == 1 + file = List.first(files) + + assert file.name == "protobuf/protoc/ext_test/extension.pb.ex", + "File path should respect module_prefix. Expected 'protobuf/protoc/ext_test/extension.pb.ex' to match module 'Protobuf.Protoc.ExtTest.*', but got '#{file.name}'" + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 41378669..57c30a14 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -14,9 +14,41 @@ defmodule Protobuf.TestHelpers do end def read_generated_file(relative_path) do - [__DIR__, "../generated", relative_path] - |> Path.join() - |> File.read!() + base_path = [__DIR__, "../generated", relative_path] |> Path.join() + + if File.exists?(base_path) do + File.read!(base_path) + else + filename = Path.basename(relative_path) + generated_dir = [__DIR__, "../generated"] |> Path.join() + + case find_file_in_dir(generated_dir, filename) do + {:ok, path} -> File.read!(path) + :not_found -> File.read!(base_path) + end + end + end + + defp find_file_in_dir(dir, filename) do + case File.ls(dir) do + {:ok, entries} -> + Enum.find_value(entries, :not_found, fn entry -> + path = Path.join(dir, entry) + + cond do + entry == filename -> {:ok, path} + File.dir?(path) -> + case find_file_in_dir(path, filename) do + {:ok, found_path} -> {:ok, found_path} + :not_found -> nil + end + true -> nil + end + end) + + {:error, _} -> + :not_found + end end def get_type_spec_as_string(module, bytecode, type)