diff --git a/cmd/protoc-gen-elixir-grpc/README.md b/cmd/protoc-gen-elixir-grpc/README.md index c5581ca..d595d4c 100644 --- a/cmd/protoc-gen-elixir-grpc/README.md +++ b/cmd/protoc-gen-elixir-grpc/README.md @@ -35,6 +35,66 @@ plugins: Server modules are generated into the same directory structure as the protobuf definitions, with `.server.pb.ex` suffix. +### Configuration Options + +You can configure the plugin using parameters: + +#### HTTP Transcoding + +Enable HTTP/JSON transcoding support for your gRPC services: + +```yaml +version: v2 +plugins: + - local: protoc-gen-elixir + out: lib + - local: protoc-gen-elixir-grpc + out: lib + opt: + - http_transcode=true +``` + +This generates: + +```elixir +defmodule Greeter.Server do + use GRPC.Server, service: Greeter.Service, http_transcode: true + + # ... method delegates +end +``` + +#### Custom Handler Module Prefix + +Organize handlers under a custom module prefix instead of using the protobuf package: + +```yaml +version: v2 +plugins: + - local: protoc-gen-elixir + out: lib + - local: protoc-gen-elixir-grpc + out: lib + opt: + - handler_module_prefix=MyApp.Handlers +``` + +#### Combining Options + +You can combine multiple options: + +```yaml +version: v2 +plugins: + - local: protoc-gen-elixir + out: lib + - local: protoc-gen-elixir-grpc + out: lib + opt: + - http_transcode=true + - handler_module_prefix=MyApp.Handlers +``` + ### Basic Service Implementation For more realistic applications that require dependencies like database connections, implement handlers diff --git a/cmd/protoc-gen-elixir-grpc/main.go b/cmd/protoc-gen-elixir-grpc/main.go index 6b29fe0..9262b86 100644 --- a/cmd/protoc-gen-elixir-grpc/main.go +++ b/cmd/protoc-gen-elixir-grpc/main.go @@ -53,8 +53,9 @@ const ( defaultPackagePrefix = "" packagePrefixFlag = "package_prefix" handlerModulePrefixFlag = "handler_module_prefix" + httpTranscodeFlag = "http_transcode" - usage = "\n\nFlags:\n -h, --help\tPrint this help and exit.\n --version\tPrint the version and exit.\n --handler_module_prefix\tCustom Elixir module prefix for handler modules instead of protobuf package." + usage = "\n\nFlags:\n -h, --help\tPrint this help and exit.\n --version\tPrint the version and exit.\n --handler_module_prefix\tCustom Elixir module prefix for handler modules instead of protobuf package.\n --http_transcode\tEnable HTTP transcoding support (adds http_transcode: true to use GRPC.Server)." ) func parsePluginParameters(paramStr string, flagSet *flag.FlagSet) error { @@ -111,6 +112,11 @@ func main() { "", "Custom Elixir module prefix for handler modules instead of protobuf package (e.g., 'MyApp.Handlers').", ) + httpTranscode := flagSet.Bool( + httpTranscodeFlag, + false, + "Enable HTTP transcoding support (adds http_transcode: true to use GRPC.Server).", + ) input, err := io.ReadAll(os.Stdin) if err != nil { @@ -162,7 +168,7 @@ func main() { continue } - generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix) + generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix, *httpTranscode) } output, err := proto.Marshal(resp) @@ -177,7 +183,7 @@ func main() { } } -func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb.FileDescriptorProto, packagePrefix, handlerModulePrefix string) { +func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb.FileDescriptorProto, packagePrefix, handlerModulePrefix string, httpTranscode bool) { if len(file.Service) == 0 { return } @@ -191,7 +197,7 @@ func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb content.WriteString("\n") for _, service := range file.Service { - generateServiceModule(&content, file, service, handlerModulePrefix) + generateServiceModule(&content, file, service, handlerModulePrefix, httpTranscode) content.WriteString("\n") } @@ -201,13 +207,16 @@ func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb }) } -func generateServiceModule(content *strings.Builder, file *descriptorpb.FileDescriptorProto, service *descriptorpb.ServiceDescriptorProto, handlerModulePrefix string) { +func generateServiceModule(content *strings.Builder, file *descriptorpb.FileDescriptorProto, service *descriptorpb.ServiceDescriptorProto, handlerModulePrefix string, httpTranscode bool) { serverModuleName := generateServerModuleName(file, service) serviceModuleName := generateServiceModuleName(file, service) content.WriteString("defmodule " + serverModuleName + " do\n") - content.WriteString(" use GRPC.Server, service: " + serviceModuleName + "\n") - content.WriteString("\n") + content.WriteString(" use GRPC.Server, service: " + serviceModuleName) + if httpTranscode { + content.WriteString(", http_transcode: true") + } + content.WriteString("\n\n") for _, method := range service.Method { generateMethodDelegate(content, file, service, method, handlerModulePrefix) diff --git a/cmd/protoc-gen-elixir-grpc/main_test.go b/cmd/protoc-gen-elixir-grpc/main_test.go index ea464dc..f924bae 100644 --- a/cmd/protoc-gen-elixir-grpc/main_test.go +++ b/cmd/protoc-gen-elixir-grpc/main_test.go @@ -151,15 +151,15 @@ end Parameter: ptr("handler_module_prefix=MyApp.Handlers"), } - rsp := testGenerate(t, req) - assert.Nil(t, rsp.Error) - assert.Equal(t, 1, len(rsp.File)) + rsp := testGenerate(t, req) + assert.Nil(t, rsp.Error) + assert.Equal(t, 1, len(rsp.File)) - file := rsp.File[0] - assert.Equal(t, "auth/v1/auth.server.pb.ex", file.GetName()) + file := rsp.File[0] + assert.Equal(t, "auth/v1/auth.server.pb.ex", file.GetName()) - content := file.GetContent() - expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. + content := file.GetContent() + expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. # # Source: auth/v1/auth.proto @@ -169,7 +169,58 @@ defmodule Auth.V1.AuthService.Server do defdelegate login(request, stream), to: MyApp.Handlers.Auth.V1.AuthService.Server.LoginHandler, as: :handle_message end ` - assert.Equal(t, expected, content) + assert.Equal(t, expected, content) + }) + + t.Run("with http_transcode option", func(t *testing.T) { + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: ptr("api/v1/api.proto"), + Package: ptr("api.v1"), + MessageType: []*descriptorpb.DescriptorProto{ + {Name: ptr("GetRequest")}, + {Name: ptr("GetResponse")}, + }, + Service: []*descriptorpb.ServiceDescriptorProto{ + { + Name: ptr("ApiService"), + Method: []*descriptorpb.MethodDescriptorProto{ + { + Name: ptr("Get"), + InputType: ptr(".api.v1.GetRequest"), + OutputType: ptr(".api.v1.GetResponse"), + }, + }, + }, + }, + } + + req := &pluginpb.CodeGeneratorRequest{ + FileToGenerate: []string{"api/v1/api.proto"}, + ProtoFile: []*descriptorpb.FileDescriptorProto{fileDesc}, + SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{fileDesc}, + CompilerVersion: compilerVersion, + Parameter: ptr("http_transcode=true"), + } + + rsp := testGenerate(t, req) + assert.Nil(t, rsp.Error) + assert.Equal(t, 1, len(rsp.File)) + + file := rsp.File[0] + assert.Equal(t, "api/v1/api.server.pb.ex", file.GetName()) + + content := file.GetContent() + expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. +# +# Source: api/v1/api.proto + +defmodule Api.V1.ApiService.Server do + use GRPC.Server, service: Api.V1.ApiService.Service, http_transcode: true + + defdelegate get(request, stream), to: Api.V1.ApiService.Server.GetHandler, as: :handle_message +end +` + assert.Equal(t, expected, content) }) t.Run("with package prefix", func(t *testing.T) { @@ -202,15 +253,15 @@ end Parameter: ptr("package_prefix=lib/proto"), } - rsp := testGenerate(t, req) - assert.Nil(t, rsp.Error) - assert.Equal(t, 1, len(rsp.File)) + rsp := testGenerate(t, req) + assert.Nil(t, rsp.Error) + assert.Equal(t, 1, len(rsp.File)) - file := rsp.File[0] - assert.Equal(t, "lib/proto/user/v1/user.server.pb.ex", file.GetName()) + file := rsp.File[0] + assert.Equal(t, "lib/proto/user/v1/user.server.pb.ex", file.GetName()) - content := file.GetContent() - expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. + content := file.GetContent() + expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. # # Source: user.proto @@ -220,8 +271,8 @@ defmodule User.V1.UserService.Server do defdelegate get_user(request, stream), to: User.V1.UserService.Server.GetUserHandler, as: :handle_message end ` - assert.Equal(t, expected, content) - }) + assert.Equal(t, expected, content) + }) t.Run("with multiple parameters", func(t *testing.T) { fileDesc := &descriptorpb.FileDescriptorProto{ @@ -270,6 +321,57 @@ defmodule Billing.V1.BillingService.Server do defdelegate create_invoice(request, stream), to: MyApp.BusinessLogic.Billing.V1.BillingService.Server.CreateInvoiceHandler, as: :handle_message end +` + assert.Equal(t, expected, content) + }) + + t.Run("with http_transcode and handler_module_prefix", func(t *testing.T) { + fileDesc := &descriptorpb.FileDescriptorProto{ + Name: ptr("payment/v1/payment.proto"), + Package: ptr("payment.v1"), + MessageType: []*descriptorpb.DescriptorProto{ + {Name: ptr("ProcessRequest")}, + {Name: ptr("ProcessResponse")}, + }, + Service: []*descriptorpb.ServiceDescriptorProto{ + { + Name: ptr("PaymentService"), + Method: []*descriptorpb.MethodDescriptorProto{ + { + Name: ptr("ProcessPayment"), + InputType: ptr(".payment.v1.ProcessRequest"), + OutputType: ptr(".payment.v1.ProcessResponse"), + }, + }, + }, + }, + } + + req := &pluginpb.CodeGeneratorRequest{ + FileToGenerate: []string{"payment/v1/payment.proto"}, + ProtoFile: []*descriptorpb.FileDescriptorProto{fileDesc}, + SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{fileDesc}, + CompilerVersion: compilerVersion, + Parameter: ptr("http_transcode=true,handler_module_prefix=MyApp.Core"), + } + + rsp := testGenerate(t, req) + assert.Nil(t, rsp.Error) + assert.Equal(t, 1, len(rsp.File)) + + file := rsp.File[0] + assert.Equal(t, "payment/v1/payment.server.pb.ex", file.GetName()) + + content := file.GetContent() + expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT. +# +# Source: payment/v1/payment.proto + +defmodule Payment.V1.PaymentService.Server do + use GRPC.Server, service: Payment.V1.PaymentService.Service, http_transcode: true + + defdelegate process_payment(request, stream), to: MyApp.Core.Payment.V1.PaymentService.Server.ProcessPaymentHandler, as: :handle_message +end ` assert.Equal(t, expected, content) })