From f31c231a39d3d1d466dc58867dda034bd02e17dc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 18 Sep 2025 17:53:14 -0400 Subject: [PATCH] fix: using the go import path Signed-off-by: Yordis Prieto --- .../main.go | 71 +++++++++++++------ .../main_test.go | 61 ++++++++++++++++ 2 files changed, 109 insertions(+), 23 deletions(-) diff --git a/cmd/protoc-gen-connect-go-servicestruct/main.go b/cmd/protoc-gen-connect-go-servicestruct/main.go index f1870a6..4f2d393 100644 --- a/cmd/protoc-gen-connect-go-servicestruct/main.go +++ b/cmd/protoc-gen-connect-go-servicestruct/main.go @@ -27,9 +27,12 @@ package main import ( + "flag" "fmt" + "go/token" "os" "path" + "path/filepath" "strings" connect "connectrpc.com/connect" @@ -42,9 +45,11 @@ const ( contextPackage = protogen.GoImportPath("context") connectPackage = protogen.GoImportPath("connectrpc.com/connect") - filenameSuffix = ".servicestruct.connect.go" - handlerFuncSuffix = "HandlerFunc" - serviceSuffix = "Struct" + filenameSuffix = ".servicestruct.connect.go" + handlerFuncSuffix = "HandlerFunc" + serviceSuffix = "Struct" + defaultPackageSuffix = "connect" + packageSuffixFlagName = "package_suffix" usage = "\n\nFlags:\n -h, --help\tPrint this help and exit.\n --version\tPrint the version and exit." ) @@ -68,15 +73,21 @@ func main() { } os.Exit(1) } - protogen.Options{}.Run( + var flagSet flag.FlagSet + packageSuffix := flagSet.String( + packageSuffixFlagName, + defaultPackageSuffix, + "Generate files into a sub-package of the package containing the base .pb.go files using the given suffix. An empty suffix denotes to generate into the same package as the base pb.go files.", + ) + protogen.Options{ParamFunc: flagSet.Set}.Run( func(plugin *protogen.Plugin) error { plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) | uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) plugin.SupportedEditionsMinimum = descriptorpb.Edition_EDITION_PROTO2 - plugin.SupportedEditionsMaximum = descriptorpb.Edition_EDITION_2023 + plugin.SupportedEditionsMaximum = descriptorpb.Edition_EDITION_2024 for _, file := range plugin.Files { - if file.Generate && len(file.Services) > 0 { - generateFile(plugin, file) + if file.Generate { + generateFile(plugin, file, *packageSuffix) } } @@ -85,28 +96,42 @@ func main() { ) } -func generateFile(plugin *protogen.Plugin, file *protogen.File) { - // Modify package name and paths to match connect plugin behavior - connectSuffix := "connect" - connectPackageName := file.GoPackageName + protogen.GoPackageName(connectSuffix) - dir := path.Dir(file.GeneratedFilenamePrefix) - base := path.Base(file.GeneratedFilenamePrefix) - connectDir := string(file.GoPackageName) + connectSuffix - filename := path.Join(dir, connectDir, base+filenameSuffix) - goImportPath := protogen.GoImportPath(path.Join( - string(file.GoImportPath), - connectDir, - )) - generatedFile := plugin.NewGeneratedFile(filename, goImportPath) +func generateFile(plugin *protogen.Plugin, file *protogen.File, packageSuffix string) { + if len(file.Services) == 0 { + return + } - // Import the base package for message types - generatedFile.Import(file.GoImportPath) + goImportPath := file.GoImportPath + if packageSuffix != "" { + if !token.IsIdentifier(packageSuffix) { + plugin.Error(fmt.Errorf("package_suffix %q is not a valid Go identifier", packageSuffix)) + return + } + file.GoPackageName += protogen.GoPackageName(packageSuffix) + generatedFilenamePrefixToSlash := filepath.ToSlash(file.GeneratedFilenamePrefix) + file.GeneratedFilenamePrefix = path.Join( + path.Dir(generatedFilenamePrefixToSlash), + string(file.GoPackageName), + path.Base(generatedFilenamePrefixToSlash), + ) + goImportPath = protogen.GoImportPath(path.Join( + string(file.GoImportPath), + string(file.GoPackageName), + )) + } + generatedFile := plugin.NewGeneratedFile( + file.GeneratedFilenamePrefix+filenameSuffix, + goImportPath, + ) + if packageSuffix != "" { + generatedFile.Import(file.GoImportPath) + } generatedFile.P("// Code generated by protoc-gen-connect-go-servicestruct. DO NOT EDIT.") generatedFile.P("//") generatedFile.P("// Source: ", file.Desc.Path()) generatedFile.P() - generatedFile.P("package ", connectPackageName) + generatedFile.P("package ", file.GoPackageName) generatedFile.P() generateHandlerFuncTypes(generatedFile, []*protogen.File{file}) diff --git a/cmd/protoc-gen-connect-go-servicestruct/main_test.go b/cmd/protoc-gen-connect-go-servicestruct/main_test.go index 03b8c29..98ba40e 100644 --- a/cmd/protoc-gen-connect-go-servicestruct/main_test.go +++ b/cmd/protoc-gen-connect-go-servicestruct/main_test.go @@ -94,6 +94,7 @@ func TestGenerate(t *testing.T) { file := rsp.File[0] assert.Contains(t, file.GetName(), "example.com/test/gen/genconnect/test.servicestruct.connect.go") + assert.Equal(t, "example.com/test/gen/genconnect/test.servicestruct.connect.go", file.GetName()) content := file.GetContent() @@ -232,6 +233,7 @@ func TestGeneratedCodeStructure(t *testing.T) { assert.Equal(t, 1, len(rsp.File)) file := rsp.File[0] + assert.Equal(t, "example.com/test/gen/genconnect/multi_service.servicestruct.connect.go", file.GetName()) content := file.GetContent() assert.Equal(t, `// Code generated by protoc-gen-connect-go-servicestruct. DO NOT EDIT. @@ -269,6 +271,65 @@ func (s *ProductServiceStruct) Create(ctx context.Context, req *connect.Request[ }) } +func TestBufGoPackagePrefixPathGeneration(t *testing.T) { + t.Parallel() + + compilerVersion := &pluginpb.Version{ + Major: ptr(int32(0)), + Minor: ptr(int32(0)), + Patch: ptr(int32(1)), + Suffix: ptr("test"), + } + + t.Run("with buf go_package_prefix causing path duplication", func(t *testing.T) { + t.Parallel() + + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: ptr("acme/billing/v1/invoice.proto"), + Package: ptr("acme.billing.v1"), + MessageType: []*descriptorpb.DescriptorProto{ + {Name: ptr("CreateInvoiceRequest")}, + {Name: ptr("CreateInvoiceResponse")}, + }, + Service: []*descriptorpb.ServiceDescriptorProto{ + { + Name: ptr("BillingService"), + Method: []*descriptorpb.MethodDescriptorProto{ + { + Name: ptr("CreateInvoice"), + InputType: ptr(".acme.billing.v1.CreateInvoiceRequest"), + OutputType: ptr(".acme.billing.v1.CreateInvoiceResponse"), + }, + }, + }, + }, + Options: &descriptorpb.FileOptions{ + GoPackage: ptr("github.com/acme/service/internal/grpc/pb/acme/billing/v1"), + }, + } + + req := &pluginpb.CodeGeneratorRequest{ + FileToGenerate: []string{"acme/billing/v1/invoice.proto"}, + ProtoFile: []*descriptorpb.FileDescriptorProto{fileDesc}, + SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{fileDesc}, + CompilerVersion: compilerVersion, + } + + rsp := testGenerate(t, req) + assert.Nil(t, rsp.Error) + assert.Equal(t, 1, len(rsp.File)) + + file := rsp.File[0] + expectedPath := "github.com/acme/service/internal/grpc/pb/acme/billing/v1/v1connect/invoice.servicestruct.connect.go" + assert.Equal(t, expectedPath, file.GetName()) + assert.NotContains(t, file.GetName(), "github.com/acme/service/internal/grpc/pb/github.com/acme/service/internal/grpc/pb") + + content := file.GetContent() + assert.Contains(t, content, "package v1connect") + assert.Contains(t, content, "v1 \"github.com/acme/service/internal/grpc/pb/acme/billing/v1\"") + }) +} + func testGenerate(t *testing.T, req *pluginpb.CodeGeneratorRequest) *pluginpb.CodeGeneratorResponse { t.Helper()