Skip to content

Commit 81fd7fa

Browse files
committed
feat: add service struct generation to protoc-gen-connect-go
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 84c1761 commit 81fd7fa

File tree

10 files changed

+291
-0
lines changed

10 files changed

+291
-0
lines changed

cmd/protoc-gen-connect-go/internal/testdata/defaultpackage/gen/genconnect/defaultpackage.connect.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/protoc-gen-connect-go/internal/testdata/diffpackage/gen/gendiff/diffpackage.connect.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/protoc-gen-connect-go/internal/testdata/samepackage/gen/samepackage.connect.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/protoc-gen-connect-go/internal/testdata/v1beta1service/gen/v1beta1service.connect.go

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/protoc-gen-connect-go/main.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ func generateService(g *protogen.GeneratedFile, file *protogen.File, service *pr
270270
generateClientInterface(g, service, names)
271271
generateClientImplementation(g, file, service, names)
272272
generateServerInterface(g, service, names)
273+
generateServiceImplementation(g, file, service, names)
273274
generateServerConstructor(g, file, service, names)
274275
generateUnimplementedServerImplementation(g, service, names)
275276
}
@@ -432,6 +433,74 @@ func generateServerInterface(g *protogen.GeneratedFile, service *protogen.Servic
432433
g.P()
433434
}
434435

436+
func generateServiceImplementation(g *protogen.GeneratedFile, file *protogen.File, service *protogen.Service, names names) {
437+
// Service struct - make it public
438+
wrapComments(g, names.Service, " provides access to the handlers for the ", service.Desc.FullName(), " service.")
439+
if isDeprecatedService(service) {
440+
g.P("//")
441+
deprecated(g)
442+
}
443+
g.AnnotateSymbol(names.Service, protogen.Annotation{Location: service.Location})
444+
g.P("type ", names.Service, " struct {")
445+
for _, method := range service.Methods {
446+
fieldName := method.GoName + "Func"
447+
g.AnnotateSymbol(names.Service+"."+fieldName, protogen.Annotation{Location: method.Location})
448+
leadingComments(
449+
g,
450+
method.Comments.Leading,
451+
isDeprecatedMethod(method),
452+
)
453+
// Use connect function types where available, fall back to full signatures
454+
if !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() {
455+
// Unary method - use HandlerFunc
456+
g.P(fieldName, " ", connectPackage.Ident("HandlerFunc"), "[", method.Input.GoIdent, ", ", method.Output.GoIdent, "]")
457+
} else if method.Desc.IsStreamingClient() && method.Desc.IsStreamingServer() {
458+
// Bidirectional streaming - use BidiStreamFunc
459+
g.P(fieldName, " ", connectPackage.Ident("BidiStreamFunc"), "[", method.Input.GoIdent, ", ", method.Output.GoIdent, "]")
460+
} else {
461+
// Client or server streaming - use full function signature (no dedicated func types available)
462+
g.P(fieldName, " func", serverSignatureParams(g, method, false))
463+
}
464+
}
465+
g.P("}")
466+
g.P()
467+
468+
// Generate methods that delegate to the handler functions to make the Service struct
469+
// implement the Handler interface (backwards compatible)
470+
for _, method := range service.Methods {
471+
fieldName := method.GoName + "Func"
472+
wrapComments(g, method.GoName, " calls the ", method.GoName, "Func handler.")
473+
if isDeprecatedMethod(method) {
474+
g.P("//")
475+
deprecated(g)
476+
}
477+
// Use named parameters for the method signature
478+
methodSig := method.GoName + serverSignatureParams(g, method, true /* named */)
479+
g.P("func (s *", names.Service, ") ", methodSig, " {")
480+
481+
// Handle different method types
482+
isStreamingClient := method.Desc.IsStreamingClient()
483+
isStreamingServer := method.Desc.IsStreamingServer()
484+
485+
switch {
486+
case isStreamingClient && isStreamingServer:
487+
// Bidirectional streaming: (ctx, stream)
488+
g.P("return s.", fieldName, "(ctx, stream)")
489+
case isStreamingClient && !isStreamingServer:
490+
// Client streaming: (ctx, stream)
491+
g.P("return s.", fieldName, "(ctx, stream)")
492+
case !isStreamingClient && isStreamingServer:
493+
// Server streaming: (ctx, req, stream)
494+
g.P("return s.", fieldName, "(ctx, req, stream)")
495+
default:
496+
// Unary: (ctx, req)
497+
g.P("return s.", fieldName, "(ctx, req)")
498+
}
499+
g.P("}")
500+
g.P()
501+
}
502+
}
503+
435504
func generateServerConstructor(g *protogen.GeneratedFile, file *protogen.File, service *protogen.Service, names names) {
436505
wrapComments(g, names.ServerConstructor, " builds an HTTP handler from the service implementation.",
437506
" It returns the path on which to mount the handler and the handler itself.")
@@ -667,6 +736,7 @@ type names struct {
667736
Server string
668737
ServerConstructor string
669738
UnimplementedServer string
739+
Service string
670740
}
671741

672742
func newNames(service *protogen.Service) names {
@@ -679,5 +749,6 @@ func newNames(service *protogen.Service) names {
679749
Server: fmt.Sprintf("%sHandler", base),
680750
ServerConstructor: fmt.Sprintf("New%sHandler", base),
681751
UnimplementedServer: fmt.Sprintf("Unimplemented%sHandler", base),
752+
Service: fmt.Sprintf("%sService", base),
682753
}
683754
}

cmd/protoc-gen-connect-go/main_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,120 @@ func TestClientHandler(t *testing.T) {
217217
})
218218
}
219219

220+
func TestServiceStruct(t *testing.T) {
221+
t.Parallel()
222+
ctx := context.Background()
223+
224+
t.Run("TestServiceService struct exists and works", func(t *testing.T) {
225+
t.Parallel()
226+
svc := testSamePackageService{}
227+
228+
service := &samepackage.TestServiceService{
229+
MethodFunc: svc.Method,
230+
}
231+
232+
assert.NotNil(t, service.MethodFunc)
233+
234+
// Test that the service struct implements the handler interface
235+
methodHandler := connect.NewUnaryHandler(
236+
samepackage.TestServiceMethodProcedure,
237+
service.Method, // Use the method, not the function field
238+
)
239+
240+
server := httptest.NewServer(methodHandler)
241+
defer server.Close()
242+
243+
client := samepackage.NewTestServiceClient(server.Client(), server.URL)
244+
rsp, err := client.Method(ctx, connect.NewRequest(&samepackage.Request{}))
245+
assert.Nil(t, err)
246+
assert.NotNil(t, rsp)
247+
})
248+
249+
t.Run("TestServiceService can be used for custom routing", func(t *testing.T) {
250+
t.Parallel()
251+
svc := testSamePackageService{}
252+
253+
service := &samepackage.TestServiceService{
254+
MethodFunc: svc.Method,
255+
}
256+
257+
methodHandler := connect.NewUnaryHandler(
258+
samepackage.TestServiceMethodProcedure,
259+
service.Method,
260+
)
261+
262+
mux := http.NewServeMux()
263+
mux.Handle(samepackage.TestServiceMethodProcedure, methodHandler)
264+
265+
server := httptest.NewServer(mux)
266+
defer server.Close()
267+
268+
client := samepackage.NewTestServiceClient(server.Client(), server.URL)
269+
rsp, err := client.Method(ctx, connect.NewRequest(&samepackage.Request{}))
270+
assert.Nil(t, err)
271+
assert.NotNil(t, rsp)
272+
})
273+
274+
t.Run("TestServiceService implements Handler interface (backwards compatible)", func(t *testing.T) {
275+
t.Parallel()
276+
svc := testSamePackageService{}
277+
278+
service := &samepackage.TestServiceService{
279+
MethodFunc: svc.Method,
280+
}
281+
282+
// The Service struct should implement the Handler interface
283+
var handler samepackage.TestServiceHandler = service
284+
assert.NotNil(t, handler)
285+
286+
// Test that we can use the service directly as a handler
287+
_, httpHandler := samepackage.NewTestServiceHandler(service)
288+
server := httptest.NewServer(httpHandler)
289+
defer server.Close()
290+
291+
client := samepackage.NewTestServiceClient(server.Client(), server.URL)
292+
rsp, err := client.Method(ctx, connect.NewRequest(&samepackage.Request{}))
293+
assert.Nil(t, err)
294+
assert.NotNil(t, rsp)
295+
})
296+
}
297+
298+
func TestServiceStructGeneration(t *testing.T) {
299+
t.Parallel()
300+
301+
t.Run("generated code contains Service struct", func(t *testing.T) {
302+
t.Parallel()
303+
samePackageFileDesc := protodesc.ToFileDescriptorProto(samepackage.File_samepackage_proto)
304+
compilerVersion := &pluginpb.Version{
305+
Major: ptr(int32(0)),
306+
Minor: ptr(int32(0)),
307+
Patch: ptr(int32(1)),
308+
Suffix: ptr("test"),
309+
}
310+
311+
req := &pluginpb.CodeGeneratorRequest{
312+
FileToGenerate: []string{"samepackage.proto"},
313+
Parameter: ptr("package_suffix"),
314+
ProtoFile: []*descriptorpb.FileDescriptorProto{samePackageFileDesc},
315+
SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{samePackageFileDesc},
316+
CompilerVersion: compilerVersion,
317+
}
318+
319+
rsp := testGenerate(t, req)
320+
assert.Nil(t, rsp.Error)
321+
assert.Equal(t, len(rsp.File), 1)
322+
323+
file := rsp.File[0]
324+
content := file.GetContent()
325+
326+
assert.True(t, strings.Contains(content, "type TestServiceService struct"))
327+
assert.True(t, strings.Contains(content, "MethodFunc connect.HandlerFunc[Request, Response]"))
328+
assert.True(t, strings.Contains(content, "TestServiceService provides access to the handlers"))
329+
assert.True(t, strings.Contains(content, "func (s *TestServiceService) Method(ctx context.Context, req *connect.Request[Request]) (*connect.Response[Response], error)"))
330+
assert.True(t, strings.Contains(content, "return s.MethodFunc(ctx, req)"))
331+
})
332+
}
333+
220334
func testCmpToTestdata(t *testing.T, content, path string) {
221335
t.Helper()
222336
b, err := testdata.ReadFile(path)

handler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ import (
1919
"net/http"
2020
)
2121

22+
// HandlerFunc is a function that implements the Handler interface.
23+
// It is used to create a Handler from a function.
24+
type HandlerFunc[Req, Res any] func(context.Context, *Request[Req]) (*Response[Res], error)
25+
26+
// BidiStreamFunc is a function that implements the BidiStream interface.
27+
// It is used to create a BidiStream from a function.
28+
type BidiStreamFunc[Req, Res any] func(context.Context, *BidiStream[Req, Res]) error
29+
2230
// A Handler is the server-side implementation of a single RPC defined by a
2331
// service schema.
2432
//

internal/gen/connect/collide/v1/collidev1connect/collide.connect.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/gen/connect/import/v1/importv1connect/import.connect.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/gen/connect/ping/v1/pingv1connect/ping.connect.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)