Skip to content

Commit d581e4e

Browse files
douglaswthraphael
andauthored
Streaming Interceptors (#3641)
* Add read/write streaming payload/result methods to interceptor DSL * Weave streaming interceptor stuff through codegen * Do not try to use a field from an interface for the raw payload when wrapping streaming methods * Fix golden files * Collect attributes from a method that actually has the interceptor applied; import the correct user types packages when rendering service_interceptors.go; fix an ancient typo * Fix StreamingResult accessor signature and return * Add SendWithContext and RecvWithContext methods to the stream interface generation * Progress on wrapping streams with interceptors * Finish generating the stream wrapping for interceptors; remove the Endpoint field from goa.InterceptorInfo struct since it is redundant with the next goa.Endpoint parameter sent to interceptors * Progress on adding comments and fixing tests; fix a bug where refs were defs * Finish fixing tests * Add more tests for streaming interceptor service codegen * Fix bug where the CLI ParseEndpoint method would try to wrap every method with interceptors even if they do not apply * Update InterceptorInfo comment and replace Send and Recv boolean fields with a CallType enum; organize interceptor wrappers file sections as types, functions, then methods * Update Golden files to make the Interceptor tests happy * Add a ReturnContext field to goa.InterceptorInfo to allow interceptors to modify the context returned by SendWithContext/RecvWithContext even after calling next * Scrap ReturnContext field in favor of changing the interceptor interface to return a context itself * Update Golden files to make the Interceptor tests happy again * Change goa.InterceptorInfo to an interface that generated interceptor info structs implement * Separate StreamingPayload and StreamingResult methods to Client and Server variations * Use goa.Endpoint for next again and do not return contexts from SendWithContext/ReceiveWithContext * Address lint issues * Fix lint issue --------- Co-authored-by: Raphael Simon <[email protected]>
1 parent 2cd5aaa commit d581e4e

File tree

94 files changed

+3403
-586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+3403
-586
lines changed

codegen/cli/cli.go

+17-12
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ type (
5858
Conversion string
5959
// Example is a valid command invocation, starting with the command name.
6060
Example string
61+
// Interceptors contains the data for client interceptors if any apply to the endpoint method.
62+
Interceptors *InterceptorData
6163
}
6264

6365
// InterceptorData contains the data needed to generate interceptor code.
@@ -181,22 +183,16 @@ func BuildCommandData(data *service.Data) *CommandData {
181183

182184
// BuildSubcommandData builds the data needed by CLI code generators to render
183185
// the CLI parsing of the service sub-command.
184-
func BuildSubcommandData(svcName string, m *service.MethodData, buildFunction *BuildFunctionData, flags []*FlagData) *SubcommandData {
185-
var (
186-
name string
187-
fullName string
188-
description string
189-
190-
conversion string
191-
)
186+
func BuildSubcommandData(data *service.Data, m *service.MethodData, buildFunction *BuildFunctionData, flags []*FlagData) *SubcommandData {
192187
en := m.Name
193-
name = codegen.KebabCase(en)
194-
fullName = goifyTerms(svcName, en)
195-
description = m.Description
188+
name := codegen.KebabCase(en)
189+
fullName := goifyTerms(data.Name, en)
190+
description := m.Description
196191
if description == "" {
197192
description = fmt.Sprintf("Make request to the %q endpoint", m.Name)
198193
}
199194

195+
var conversion string
200196
if m.Payload != "" && buildFunction == nil && len(flags) > 0 {
201197
// No build function, just convert the arg to the body type
202198
var convPre, convSuff string
@@ -226,6 +222,14 @@ func BuildSubcommandData(svcName string, m *service.MethodData, buildFunction *B
226222
conversion += "\n}"
227223
}
228224
}
225+
226+
var interceptors *InterceptorData
227+
if len(m.ClientInterceptors) > 0 {
228+
interceptors = &InterceptorData{
229+
VarName: "inter",
230+
PkgName: data.PkgName,
231+
}
232+
}
229233
sub := &SubcommandData{
230234
Name: name,
231235
FullName: fullName,
@@ -234,8 +238,9 @@ func BuildSubcommandData(svcName string, m *service.MethodData, buildFunction *B
234238
MethodVarName: m.VarName,
235239
BuildFunction: buildFunction,
236240
Conversion: conversion,
241+
Interceptors: interceptors,
237242
}
238-
generateExample(sub, svcName)
243+
generateExample(sub, data.Name)
239244

240245
return sub
241246
}

codegen/service/interceptors.go

+76-2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func interceptorFile(svc *Data, server bool) *codegen.File {
7878
},
7979
}
8080
if len(interceptors) > 0 {
81+
codegen.AddImport(sections[0], svc.UserTypeImports...)
8182
sections = append(sections, &codegen.SectionTemplate{
8283
Name: "interceptor-types",
8384
Source: readTemplate("interceptors_types"),
@@ -139,7 +140,34 @@ func wrapperFile(svc *Data) *codegen.File {
139140
codegen.GoaImport(""),
140141
}))
141142

142-
// Generate the interceptor wrapper functions first (only once)
143+
// Generate any interceptor stream wrapper struct types first
144+
var wrappedServerStreams, wrappedClientStreams []*StreamInterceptorData
145+
if len(svc.ServerInterceptors) > 0 {
146+
wrappedServerStreams = collectWrappedStreams(svc.ServerInterceptors, true)
147+
if len(wrappedServerStreams) > 0 {
148+
sections = append(sections, &codegen.SectionTemplate{
149+
Name: "server-interceptor-stream-wrapper-types",
150+
Source: readTemplate("server_interceptor_stream_wrapper_types"),
151+
Data: map[string]interface{}{
152+
"WrappedServerStreams": wrappedServerStreams,
153+
},
154+
})
155+
}
156+
}
157+
if len(svc.ClientInterceptors) > 0 {
158+
wrappedClientStreams = collectWrappedStreams(svc.ClientInterceptors, false)
159+
if len(wrappedClientStreams) > 0 {
160+
sections = append(sections, &codegen.SectionTemplate{
161+
Name: "client-interceptor-stream-wrapper-types",
162+
Source: readTemplate("client_interceptor_stream_wrapper_types"),
163+
Data: map[string]interface{}{
164+
"WrappedClientStreams": wrappedClientStreams,
165+
},
166+
})
167+
}
168+
}
169+
170+
// Generate the interceptor wrapper functions next (only once)
143171
if len(svc.ServerInterceptors) > 0 {
144172
sections = append(sections, &codegen.SectionTemplate{
145173
Name: "server-interceptor-wrappers",
@@ -161,6 +189,26 @@ func wrapperFile(svc *Data) *codegen.File {
161189
})
162190
}
163191

192+
// Generate any interceptor stream wrapper struct methods last
193+
if len(wrappedServerStreams) > 0 {
194+
sections = append(sections, &codegen.SectionTemplate{
195+
Name: "server-interceptor-stream-wrappers",
196+
Source: readTemplate("server_interceptor_stream_wrappers"),
197+
Data: map[string]interface{}{
198+
"WrappedServerStreams": wrappedServerStreams,
199+
},
200+
})
201+
}
202+
if len(wrappedClientStreams) > 0 {
203+
sections = append(sections, &codegen.SectionTemplate{
204+
Name: "client-interceptor-stream-wrappers",
205+
Source: readTemplate("client_interceptor_stream_wrappers"),
206+
Data: map[string]interface{}{
207+
"WrappedClientStreams": wrappedClientStreams,
208+
},
209+
})
210+
}
211+
164212
return &codegen.File{
165213
Path: path,
166214
SectionTemplates: sections,
@@ -171,9 +219,35 @@ func wrapperFile(svc *Data) *codegen.File {
171219
// private implementation types.
172220
func hasPrivateImplementationTypes(interceptors []*InterceptorData) bool {
173221
for _, intr := range interceptors {
174-
if intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil {
222+
if intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil || intr.ReadStreamingPayload != nil || intr.WriteStreamingPayload != nil || intr.ReadStreamingResult != nil || intr.WriteStreamingResult != nil {
175223
return true
176224
}
177225
}
178226
return false
179227
}
228+
229+
// collectWrappedStreams returns a slice of streams to be wrapped by interceptor wrapper functions.
230+
func collectWrappedStreams(interceptors []*InterceptorData, server bool) []*StreamInterceptorData {
231+
var (
232+
streams []*StreamInterceptorData
233+
streamNames = make(map[string]struct{})
234+
)
235+
for _, intr := range interceptors {
236+
if intr.HasStreamingPayloadAccess || intr.HasStreamingResultAccess {
237+
for _, method := range intr.Methods {
238+
if server {
239+
if _, ok := streamNames[method.ServerStream.Interface]; !ok {
240+
streams = append(streams, method.ServerStream)
241+
streamNames[method.ServerStream.Interface] = struct{}{}
242+
}
243+
} else {
244+
if _, ok := streamNames[method.ClientStream.Interface]; !ok {
245+
streams = append(streams, method.ClientStream)
246+
streamNames[method.ClientStream.Interface] = struct{}{}
247+
}
248+
}
249+
}
250+
}
251+
}
252+
return streams
253+
}

codegen/service/interceptors.md

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Interceptors Code Generation
22

3-
Goa generates interceptor code to enable request/response interception and payload/result access.
3+
Goa generates interceptor code to enable request/response interception and payload/result access.
44

55
---
66

@@ -10,8 +10,8 @@ Goa generates interceptor code to enable request/response interception and paylo
1010

1111
Client and server interceptor code is generated in:
1212

13-
- `gen/client_interceptors.go`
14-
- `gen/service_interceptors.go`
13+
* `gen/client_interceptors.go`
14+
* `gen/service_interceptors.go`
1515

1616
### Templates Used
1717

@@ -26,12 +26,13 @@ Client and server interceptor code is generated in:
2626
3. **`client_wrappers.go.tpl`** and **`endpoint_wrappers.go.tpl`**
2727
Generate code that wraps client and service endpoints with interceptor callbacks.
2828
Each template takes a map with:
29+
2930
```go
3031
map[string]any{
31-
"MethodVarName": <Implemented method name>
32-
"Method": <Design method name>
33-
"Service": <Service name>
34-
"Interceptors": <Slice of InterceptorData>
32+
"MethodVarName": <Implemented method name>
33+
"Method": <Design method name>
34+
"Service": <Service name>
35+
"Interceptors": <Slice of InterceptorData>
3536
}
3637
```
3738

@@ -43,12 +44,13 @@ Client and server interceptor code is generated in:
4344

4445
Endpoint wrapper code for both client and server interceptors is generated in:
4546

46-
- `gen/interceptor_wrappers.go`
47+
* `gen/interceptor_wrappers.go`
4748

4849
### Templates Used
4950

5051
1. **`server_interceptor_wrappers.go.tpl`**
5152
Generates server-specific wrapper implementations. This template receives a map with:
53+
5254
```go
5355
map[string]any{
5456
"Service": svc.Name,
@@ -58,6 +60,7 @@ Endpoint wrapper code for both client and server interceptors is generated in:
5860

5961
2. **`client_interceptor_wrappers.go.tpl`**
6062
Generates client-specific wrapper implementations. This template receives a map with:
63+
6164
```go
6265
map[string]any{
6366
"Service": svc.Name,
@@ -78,6 +81,7 @@ Example interceptors are generated by the example command in an interceptors sub
7881

7982
1. **`example_client_interceptor.go.tpl` and `example_server_interceptor.go.tpl`**
8083
Generate example interceptor implementations. Each template takes a map with:
84+
8185
```go
8286
map[string]any{
8387
"StructName": <interceptor struct name>
@@ -122,10 +126,16 @@ The main structure describing each interceptor’s metadata and requirements:
122126
* `Description`: Interceptor description from the design
123127
* `HasPayloadAccess`: Indicates if any method requires payload access
124128
* `HasResultAccess`: Indicates if any method requires result access
129+
* `HasStreamingPayloadAccess`: Indicates if any method requires streaming payload access
130+
* `HasStreamingResultAccess`: Indicates if any method requires streaming result access
125131
* `ReadPayload`: List of readable payload fields ([]AttributeData)
126132
* `WritePayload`: List of writable payload fields ([]AttributeData)
127133
* `ReadResult`: List of readable result fields ([]AttributeData)
128134
* `WriteResult`: List of writable result fields ([]AttributeData)
135+
* `ReadStreamingPayload`: List of readable streaming payload fields ([]AttributeData)
136+
* `WriteStreamingPayload`: List of writable streaming payload fields ([]AttributeData)
137+
* `ReadStreamingResult`: List of readable streaming result fields ([]AttributeData)
138+
* `WriteStreamingResult`: List of writable streaming result fields ([]AttributeData)
129139
* `Methods`: A list of MethodInterceptorData containing method-specific interceptor information
130140
* `ServerStreamInputStruct`: Server stream variable name (used if streaming)
131141
* `ClientStreamInputStruct`: Client stream variable name (used if streaming)
@@ -137,8 +147,12 @@ Stores per-method interceptor configuration:
137147
* `MethodName`: The method’s Go variable name
138148
* `PayloadAccess`: Name of the payload access type
139149
* `ResultAccess`: Name of the result access type
150+
* `StreamingPayloadAccess`: Name of the streaming payload access type
151+
* `StreamingResultAccess`: Name of the streaming result access type
140152
* `PayloadRef`: Reference to the method's payload type
141153
* `ResultRef`: Reference to the method's result type
154+
* `StreamingPayloadRef`: Reference to the method's streaming payload type
155+
* `StreamingResultRef`: Reference to the method's streaming result type
142156

143157
### `AttributeData`
144158

codegen/service/interceptors_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ func TestInterceptors(t *testing.T) {
4242
{"interceptor-with-read-result", testdata.InterceptorWithReadResultDSL, 3},
4343
{"interceptor-with-write-result", testdata.InterceptorWithWriteResultDSL, 3},
4444
{"interceptor-with-read-write-result", testdata.InterceptorWithReadWriteResultDSL, 3},
45-
{"streaming-interceptors", testdata.StreamingInterceptorsDSL, 2},
45+
{"streaming-interceptors", testdata.StreamingInterceptorsDSL, 3},
46+
{"streaming-interceptors-with-read-payload-and-read-streaming-payload", testdata.StreamingInterceptorsWithReadPayloadAndReadStreamingPayloadDSL, 3},
47+
{"streaming-interceptors-with-read-streaming-result", testdata.StreamingInterceptorsWithReadStreamingResultDSL, 3},
4648
{"streaming-interceptors-with-read-payload", testdata.StreamingInterceptorsWithReadPayloadDSL, 2},
4749
{"streaming-interceptors-with-read-result", testdata.StreamingInterceptorsWithReadResultDSL, 2},
4850
}

codegen/service/service.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s
4747
if m.StreamingPayloadDef != "" {
4848
if _, ok := seen[m.StreamingPayload]; !ok {
4949
addTypeDefSection(payloadPath, m.StreamingPayload, &codegen.SectionTemplate{
50-
Name: "service-streamig-payload",
50+
Name: "service-streaming-payload",
5151
Source: readTemplate("streaming_payload"),
5252
Data: m,
5353
})

0 commit comments

Comments
 (0)