Skip to content

Commit c5dce2f

Browse files
committed
feat: add HTTP transcoding support and update tests
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent b11eb7b commit c5dce2f

File tree

3 files changed

+195
-24
lines changed

3 files changed

+195
-24
lines changed

cmd/protoc-gen-elixir-grpc/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,66 @@ plugins:
3535
3636
Server modules are generated into the same directory structure as the protobuf definitions, with `.server.pb.ex` suffix.
3737

38+
### Configuration Options
39+
40+
You can configure the plugin using parameters:
41+
42+
#### HTTP Transcoding
43+
44+
Enable HTTP/JSON transcoding support for your gRPC services:
45+
46+
```yaml
47+
version: v2
48+
plugins:
49+
- local: protoc-gen-elixir
50+
out: lib
51+
- local: protoc-gen-elixir-grpc
52+
out: lib
53+
opt:
54+
- http_transcode=true
55+
```
56+
57+
This generates:
58+
59+
```elixir
60+
defmodule Greeter.Server do
61+
use GRPC.Server, service: Greeter.Service, http_transcode: true
62+
63+
# ... method delegates
64+
end
65+
```
66+
67+
#### Custom Handler Module Prefix
68+
69+
Organize handlers under a custom module prefix instead of using the protobuf package:
70+
71+
```yaml
72+
version: v2
73+
plugins:
74+
- local: protoc-gen-elixir
75+
out: lib
76+
- local: protoc-gen-elixir-grpc
77+
out: lib
78+
opt:
79+
- handler_module_prefix=MyApp.Handlers
80+
```
81+
82+
#### Combining Options
83+
84+
You can combine multiple options:
85+
86+
```yaml
87+
version: v2
88+
plugins:
89+
- local: protoc-gen-elixir
90+
out: lib
91+
- local: protoc-gen-elixir-grpc
92+
out: lib
93+
opt:
94+
- http_transcode=true
95+
- handler_module_prefix=MyApp.Handlers
96+
```
97+
3898
### Basic Service Implementation
3999

40100
For more realistic applications that require dependencies like database connections, implement handlers

cmd/protoc-gen-elixir-grpc/main.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ const (
5353
defaultPackagePrefix = ""
5454
packagePrefixFlag = "package_prefix"
5555
handlerModulePrefixFlag = "handler_module_prefix"
56+
httpTranscodeFlag = "http_transcode"
5657

57-
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."
58+
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)."
5859
)
5960

6061
func parsePluginParameters(paramStr string, flagSet *flag.FlagSet) error {
@@ -111,6 +112,11 @@ func main() {
111112
"",
112113
"Custom Elixir module prefix for handler modules instead of protobuf package (e.g., 'MyApp.Handlers').",
113114
)
115+
httpTranscode := flagSet.Bool(
116+
httpTranscodeFlag,
117+
false,
118+
"Enable HTTP transcoding support (adds http_transcode: true to use GRPC.Server).",
119+
)
114120

115121
input, err := io.ReadAll(os.Stdin)
116122
if err != nil {
@@ -162,7 +168,7 @@ func main() {
162168
continue
163169
}
164170

165-
generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix)
171+
generateElixirFile(resp, protoFile, *packagePrefix, *handlerModulePrefix, *httpTranscode)
166172
}
167173

168174
output, err := proto.Marshal(resp)
@@ -177,7 +183,7 @@ func main() {
177183
}
178184
}
179185

180-
func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb.FileDescriptorProto, packagePrefix, handlerModulePrefix string) {
186+
func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb.FileDescriptorProto, packagePrefix, handlerModulePrefix string, httpTranscode bool) {
181187
if len(file.Service) == 0 {
182188
return
183189
}
@@ -191,7 +197,7 @@ func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb
191197
content.WriteString("\n")
192198

193199
for _, service := range file.Service {
194-
generateServiceModule(&content, file, service, handlerModulePrefix)
200+
generateServiceModule(&content, file, service, handlerModulePrefix, httpTranscode)
195201
content.WriteString("\n")
196202
}
197203

@@ -201,13 +207,16 @@ func generateElixirFile(resp *pluginpb.CodeGeneratorResponse, file *descriptorpb
201207
})
202208
}
203209

204-
func generateServiceModule(content *strings.Builder, file *descriptorpb.FileDescriptorProto, service *descriptorpb.ServiceDescriptorProto, handlerModulePrefix string) {
210+
func generateServiceModule(content *strings.Builder, file *descriptorpb.FileDescriptorProto, service *descriptorpb.ServiceDescriptorProto, handlerModulePrefix string, httpTranscode bool) {
205211
serverModuleName := generateServerModuleName(file, service)
206212
serviceModuleName := generateServiceModuleName(file, service)
207213

208214
content.WriteString("defmodule " + serverModuleName + " do\n")
209-
content.WriteString(" use GRPC.Server, service: " + serviceModuleName + "\n")
210-
content.WriteString("\n")
215+
content.WriteString(" use GRPC.Server, service: " + serviceModuleName)
216+
if httpTranscode {
217+
content.WriteString(", http_transcode: true")
218+
}
219+
content.WriteString("\n\n")
211220

212221
for _, method := range service.Method {
213222
generateMethodDelegate(content, file, service, method, handlerModulePrefix)

cmd/protoc-gen-elixir-grpc/main_test.go

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,15 @@ end
151151
Parameter: ptr("handler_module_prefix=MyApp.Handlers"),
152152
}
153153

154-
rsp := testGenerate(t, req)
155-
assert.Nil(t, rsp.Error)
156-
assert.Equal(t, 1, len(rsp.File))
154+
rsp := testGenerate(t, req)
155+
assert.Nil(t, rsp.Error)
156+
assert.Equal(t, 1, len(rsp.File))
157157

158-
file := rsp.File[0]
159-
assert.Equal(t, "auth/v1/auth.server.pb.ex", file.GetName())
158+
file := rsp.File[0]
159+
assert.Equal(t, "auth/v1/auth.server.pb.ex", file.GetName())
160160

161-
content := file.GetContent()
162-
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
161+
content := file.GetContent()
162+
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
163163
#
164164
# Source: auth/v1/auth.proto
165165
@@ -169,7 +169,58 @@ defmodule Auth.V1.AuthService.Server do
169169
defdelegate login(request, stream), to: MyApp.Handlers.Auth.V1.AuthService.Server.LoginHandler, as: :handle_message
170170
end
171171
`
172-
assert.Equal(t, expected, content)
172+
assert.Equal(t, expected, content)
173+
})
174+
175+
t.Run("with http_transcode option", func(t *testing.T) {
176+
fileDesc := &descriptorpb.FileDescriptorProto{
177+
Name: ptr("api/v1/api.proto"),
178+
Package: ptr("api.v1"),
179+
MessageType: []*descriptorpb.DescriptorProto{
180+
{Name: ptr("GetRequest")},
181+
{Name: ptr("GetResponse")},
182+
},
183+
Service: []*descriptorpb.ServiceDescriptorProto{
184+
{
185+
Name: ptr("ApiService"),
186+
Method: []*descriptorpb.MethodDescriptorProto{
187+
{
188+
Name: ptr("Get"),
189+
InputType: ptr(".api.v1.GetRequest"),
190+
OutputType: ptr(".api.v1.GetResponse"),
191+
},
192+
},
193+
},
194+
},
195+
}
196+
197+
req := &pluginpb.CodeGeneratorRequest{
198+
FileToGenerate: []string{"api/v1/api.proto"},
199+
ProtoFile: []*descriptorpb.FileDescriptorProto{fileDesc},
200+
SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{fileDesc},
201+
CompilerVersion: compilerVersion,
202+
Parameter: ptr("http_transcode=true"),
203+
}
204+
205+
rsp := testGenerate(t, req)
206+
assert.Nil(t, rsp.Error)
207+
assert.Equal(t, 1, len(rsp.File))
208+
209+
file := rsp.File[0]
210+
assert.Equal(t, "api/v1/api.server.pb.ex", file.GetName())
211+
212+
content := file.GetContent()
213+
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
214+
#
215+
# Source: api/v1/api.proto
216+
217+
defmodule Api.V1.ApiService.Server do
218+
use GRPC.Server, service: Api.V1.ApiService.Service, http_transcode: true
219+
220+
defdelegate get(request, stream), to: Api.V1.ApiService.Server.GetHandler, as: :handle_message
221+
end
222+
`
223+
assert.Equal(t, expected, content)
173224
})
174225

175226
t.Run("with package prefix", func(t *testing.T) {
@@ -202,15 +253,15 @@ end
202253
Parameter: ptr("package_prefix=lib/proto"),
203254
}
204255

205-
rsp := testGenerate(t, req)
206-
assert.Nil(t, rsp.Error)
207-
assert.Equal(t, 1, len(rsp.File))
256+
rsp := testGenerate(t, req)
257+
assert.Nil(t, rsp.Error)
258+
assert.Equal(t, 1, len(rsp.File))
208259

209-
file := rsp.File[0]
210-
assert.Equal(t, "lib/proto/user/v1/user.server.pb.ex", file.GetName())
260+
file := rsp.File[0]
261+
assert.Equal(t, "lib/proto/user/v1/user.server.pb.ex", file.GetName())
211262

212-
content := file.GetContent()
213-
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
263+
content := file.GetContent()
264+
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
214265
#
215266
# Source: user.proto
216267
@@ -220,8 +271,8 @@ defmodule User.V1.UserService.Server do
220271
defdelegate get_user(request, stream), to: User.V1.UserService.Server.GetUserHandler, as: :handle_message
221272
end
222273
`
223-
assert.Equal(t, expected, content)
224-
})
274+
assert.Equal(t, expected, content)
275+
})
225276

226277
t.Run("with multiple parameters", func(t *testing.T) {
227278
fileDesc := &descriptorpb.FileDescriptorProto{
@@ -270,6 +321,57 @@ defmodule Billing.V1.BillingService.Server do
270321
271322
defdelegate create_invoice(request, stream), to: MyApp.BusinessLogic.Billing.V1.BillingService.Server.CreateInvoiceHandler, as: :handle_message
272323
end
324+
`
325+
assert.Equal(t, expected, content)
326+
})
327+
328+
t.Run("with http_transcode and handler_module_prefix", func(t *testing.T) {
329+
fileDesc := &descriptorpb.FileDescriptorProto{
330+
Name: ptr("payment/v1/payment.proto"),
331+
Package: ptr("payment.v1"),
332+
MessageType: []*descriptorpb.DescriptorProto{
333+
{Name: ptr("ProcessRequest")},
334+
{Name: ptr("ProcessResponse")},
335+
},
336+
Service: []*descriptorpb.ServiceDescriptorProto{
337+
{
338+
Name: ptr("PaymentService"),
339+
Method: []*descriptorpb.MethodDescriptorProto{
340+
{
341+
Name: ptr("ProcessPayment"),
342+
InputType: ptr(".payment.v1.ProcessRequest"),
343+
OutputType: ptr(".payment.v1.ProcessResponse"),
344+
},
345+
},
346+
},
347+
},
348+
}
349+
350+
req := &pluginpb.CodeGeneratorRequest{
351+
FileToGenerate: []string{"payment/v1/payment.proto"},
352+
ProtoFile: []*descriptorpb.FileDescriptorProto{fileDesc},
353+
SourceFileDescriptors: []*descriptorpb.FileDescriptorProto{fileDesc},
354+
CompilerVersion: compilerVersion,
355+
Parameter: ptr("http_transcode=true,handler_module_prefix=MyApp.Core"),
356+
}
357+
358+
rsp := testGenerate(t, req)
359+
assert.Nil(t, rsp.Error)
360+
assert.Equal(t, 1, len(rsp.File))
361+
362+
file := rsp.File[0]
363+
assert.Equal(t, "payment/v1/payment.server.pb.ex", file.GetName())
364+
365+
content := file.GetContent()
366+
expected := `# Code generated by protoc-gen-elixir-grpc. DO NOT EDIT.
367+
#
368+
# Source: payment/v1/payment.proto
369+
370+
defmodule Payment.V1.PaymentService.Server do
371+
use GRPC.Server, service: Payment.V1.PaymentService.Service, http_transcode: true
372+
373+
defdelegate process_payment(request, stream), to: MyApp.Core.Payment.V1.PaymentService.Server.ProcessPaymentHandler, as: :handle_message
374+
end
273375
`
274376
assert.Equal(t, expected, content)
275377
})

0 commit comments

Comments
 (0)