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
71 changes: 48 additions & 23 deletions cmd/protoc-gen-connect-go-servicestruct/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
package main

import (
"flag"
"fmt"
"go/token"
"os"
"path"
"path/filepath"
"strings"

connect "connectrpc.com/connect"
Expand All @@ -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."
)
Expand All @@ -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)
}
}

Expand All @@ -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})
Expand Down
61 changes: 61 additions & 0 deletions cmd/protoc-gen-connect-go-servicestruct/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand Down