Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 12 additions & 11 deletions cmd/protoc-gen-elixir/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ func renderFileExtensionGroups(files []*descriptorpb.FileDescriptorProto, params
// both a service AND a message-embedded extend in the same file, so the
// exact service-vs-this-tier relative order is inferred from that
// original table, not independently proven.
// 5. Services (gated on plugins=grpc), in file declaration order, each as a
// paired ".Service" + ".Stub" module - rendered after every message body,
// verified against testdata/golden/grpc/test/service.pb.ex and
// 5. Services (gated on plugins=grpc), in file declaration order, each as
// one module pairing its ".Service" and ".Stub" defmodules - rendered
// after every message body, verified against
// testdata/golden/grpc/test/service.pb.ex and
// testdata/golden/grpc_proto_source/test/service.pb.ex.
//
// The file-level merged PbExtension module is NOT rendered here at all - see
Expand Down Expand Up @@ -284,13 +285,14 @@ func (ctx *fileRenderContext) renderEnum(enum *descriptorpb.EnumDescriptorProto,

// renderService renders a single service's paired .Service/.Stub modules
// given the file's base module name / base package and the service's own
// SourceCodeInfo path (e.g. [6, i] for file-level service i). The two
// modules are returned as separate renderedModule entries (each with its own
// module name) since one_file_per_module=true emits them as two distinct
// files.
// SourceCodeInfo path (e.g. [6, i] for file-level service i). The pair is
// returned as ONE renderedModule keyed by the bare service module name (no
// .Service/.Stub suffix), matching the escript, which emits both defmodules
// into a single file derived from that bare name.
func (ctx *fileRenderContext) renderService(svc *descriptorpb.ServiceDescriptorProto, path []int32) ([]renderedModule, error) {
serviceModName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName())) + ".Service"
stubModName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName())) + ".Stub"
modName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName()))
serviceModName := modName + ".Service"
stubModName := modName + ".Stub"
fullName := qualifyFullName(ctx.basePkg, svc.GetName())

var docComment string
Expand All @@ -309,8 +311,7 @@ func (ctx *fileRenderContext) renderService(svc *descriptorpb.ServiceDescriptorP
}

return []renderedModule{
{modName: serviceModName, text: serviceText},
{modName: stubModName, text: stubText},
{modName: modName, text: serviceText + "\n\n" + stubText},
}, nil
}

Expand Down
32 changes: 14 additions & 18 deletions cmd/protoc-gen-elixir/one_file_per_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,15 @@ func TestOneFilePerModuleIntegration(t *testing.T) {
assert.Len(t, resp.GetFile(), len(goldenPaths), "generated file count should match golden fixture count exactly")
}

// TestOneFilePerModuleWithGRPC guards against a regression where the
// .Service/.Stub pair was split apart via strings.Cut(text, "\n\n") on
// RenderService's combined output: the .Service module body itself contains
// internal blank lines (e.g. after a multi-line @moduledoc, before "use
// GRPC.Service") that appear before the true Service/Stub boundary, so
// cutting on the first "\n\n" silently truncated the .Service file and
// prepended garbage to the .Stub file. There's no golden fixture combining
// TestOneFilePerModuleWithGRPC guards the escript-matching file layout: the
// .Service and .Stub defmodules for a given service are emitted together in
// ONE file, named from the bare service module name (no .Service/.Stub
// suffix) via Macro.underscore. There's no golden fixture combining
// one_file_per_module=true with plugins=grpc (gen_goldens.sh never generates
// one), so this asserts against testdata/golden/grpc/test/service.pb.ex -
// the proven-correct single-file rendering of the same test.proto/
// service.proto pair - split at its own known-correct module boundary.
// the proven-correct rendering of the same test.proto/service.proto pair -
// which is exactly the expected content here since that fixture already
// pairs both defmodules in one blob.
func TestOneFilePerModuleWithGRPC(t *testing.T) {
t.Parallel()

Expand All @@ -88,7 +86,7 @@ func TestOneFilePerModuleWithGRPC(t *testing.T) {
resp := testGenerate(t, req)
require.Empty(t, resp.GetError())

wantService := `defmodule Test.TestService.Service do
want := `defmodule Test.TestService.Service do
@moduledoc """
An example test service that has
a test method. It expects a Request
Expand All @@ -99,17 +97,15 @@ func TestOneFilePerModuleWithGRPC(t *testing.T) {

rpc :test, Test.Request, Test.Reply
end
`
wantStub := `defmodule Test.TestService.Stub do

defmodule Test.TestService.Stub do
use GRPC.Stub, service: Test.TestService.Service
end
`

serviceFile, ok := findGeneratedFile(resp.GetFile(), "test/test_service/service.pb.ex")
require.True(t, ok, "expected test/test_service/service.pb.ex among generated files")
assert.Equal(t, wantService, serviceFile.GetContent())
require.Len(t, resp.GetFile(), 1, "service.proto under one_file_per_module should yield exactly the merged service file, with no separate .Service/.Stub files")

stubFile, ok := findGeneratedFile(resp.GetFile(), "test/test_service/stub.pb.ex")
require.True(t, ok, "expected test/test_service/stub.pb.ex among generated files")
assert.Equal(t, wantStub, stubFile.GetContent())
file, ok := findGeneratedFile(resp.GetFile(), "test/test_service.pb.ex")
require.True(t, ok, "expected test/test_service.pb.ex among generated files")
assert.Equal(t, want, file.GetContent())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}