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
60 changes: 60 additions & 0 deletions cmd/protoc-gen-elixir-grpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 16 additions & 7 deletions cmd/protoc-gen-elixir-grpc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -162,7 +168,7 @@ func main() {
continue
}

generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix)
generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix, *httpTranscode)
}

output, err := proto.Marshal(resp)
Expand All @@ -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
}
Expand All @@ -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")
}

Expand All @@ -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)
Expand Down
136 changes: 119 additions & 17 deletions cmd/protoc-gen-elixir-grpc/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) {
Expand Down Expand Up @@ -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

Expand All @@ -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{
Expand Down Expand Up @@ -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)
})
Expand Down