From 85b416db0c6d099aabcd2f69667989ac3e31be14 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Sun, 19 Jan 2025 10:53:55 -0800 Subject: [PATCH 1/6] Merge v3 --- README.md | 1 + codegen/service/client.go | 12 + codegen/service/client_test.go | 1 + codegen/service/convert_test.go | 24 -- codegen/service/endpoint.go | 66 +++- codegen/service/endpoint_test.go | 2 + codegen/service/interceptors.go | 215 +++++++++++++ codegen/service/interceptors_test.go | 209 ++++++++++++ codegen/service/service.go | 5 + .../service/templates/client_wrappers.go.tpl | 29 ++ .../templates/endpoint_wrappers.go.tpl | 30 ++ codegen/service/templates/interceptors.go.tpl | 153 +++++++++ .../templates/service_client_init.go.tpl | 12 +- .../templates/service_endpoints_init.go.tpl | 15 +- .../templates/service_interceptor.go.tpl | 0 codegen/service/testdata/client_code.go | 47 +++ codegen/service/testdata/endpoint_code.go | 181 +++++++++++ codegen/service/testdata/endpoint_dsls.go | 44 +++ .../interceptor-with-read-payload.golden | 41 +++ .../interceptor-with-read-result.golden | 41 +++ ...interceptor-with-read-write-payload.golden | 45 +++ .../interceptor-with-read-write-result.golden | 45 +++ .../interceptor-with-write-payload.golden | 41 +++ .../interceptor-with-write-result.golden | 41 +++ .../interceptors/multiple-interceptors.golden | 30 ++ .../single-api-server-interceptor.golden | 16 + .../single-client-interceptor.golden | 16 + .../single-method-server-interceptor.golden | 16 + .../single-service-server-interceptor.golden | 16 + ...ming-interceptors-with-read-payload.golden | 45 +++ ...aming-interceptors-with-read-result.golden | 45 +++ .../streaming-interceptors.golden | 16 + codegen/service/testdata/interceptors_dsls.go | 297 ++++++++++++++++++ codegen/service/testdata/service_dsls.go | 14 + codegen/service/testing.go | 42 +++ dsl/description.go | 2 + dsl/interceptor.go | 293 +++++++++++++++++ dsl/interceptor_test.go | 250 +++++++++++++++ dsl/meta.go | 39 +++ expr/api.go | 4 + expr/interceptor.go | 84 +++++ expr/method.go | 79 ++++- expr/root.go | 2 + expr/service.go | 4 + go.mod | 16 +- go.sum | 24 +- http/codegen/service_data.go | 48 +-- pkg/interceptor.go | 22 ++ 48 files changed, 2622 insertions(+), 98 deletions(-) create mode 100644 codegen/service/interceptors.go create mode 100644 codegen/service/interceptors_test.go create mode 100644 codegen/service/templates/client_wrappers.go.tpl create mode 100644 codegen/service/templates/endpoint_wrappers.go.tpl create mode 100644 codegen/service/templates/interceptors.go.tpl create mode 100644 codegen/service/templates/service_interceptor.go.tpl create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-payload.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-result.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-payload.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-result.golden create mode 100644 codegen/service/testdata/interceptors/multiple-interceptors.golden create mode 100644 codegen/service/testdata/interceptors/single-api-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-client-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-method-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-service-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors.golden create mode 100644 codegen/service/testdata/interceptors_dsls.go create mode 100644 codegen/service/testing.go create mode 100644 dsl/interceptor.go create mode 100644 dsl/interceptor_test.go create mode 100644 expr/interceptor.go create mode 100644 pkg/interceptor.go diff --git a/README.md b/README.md index 7735bc20e3..cd38d1919e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Slack: Goa Slack: Sign-up BSky: Goa + Twitter: @goadesign

diff --git a/codegen/service/client.go b/codegen/service/client.go index a088de0d37..f991e81aed 100644 --- a/codegen/service/client.go +++ b/codegen/service/client.go @@ -45,6 +45,18 @@ func ClientFile(_ string, service *expr.ServiceExpr) *codegen.File { Source: readTemplate("service_client_method"), Data: m, }) + if len(m.ClientInterceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "client-wrapper", + Source: readTemplate("client_wrappers"), + Data: map[string]interface{}{ + "Method": m.Name, + "MethodVarName": codegen.Goify(m.Name, true), + "Service": svc.Name, + "ClientInterceptors": m.ClientInterceptors, + }, + }) + } } } diff --git a/codegen/service/client_test.go b/codegen/service/client_test.go index 2fa5152365..6cfda7d4df 100644 --- a/codegen/service/client_test.go +++ b/codegen/service/client_test.go @@ -32,6 +32,7 @@ func TestClient(t *testing.T) { {"client-streaming-payload-no-result", testdata.StreamingPayloadNoResultMethodDSL, testdata.StreamingPayloadNoResultMethodClient}, {"client-bidirectional-streaming", testdata.BidirectionalStreamingMethodDSL, testdata.BidirectionalStreamingMethodClient}, {"client-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethodClient}, + {"client-interceptor", testdata.EndpointWithClientInterceptorDSL, testdata.InterceptorClient}, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { diff --git a/codegen/service/convert_test.go b/codegen/service/convert_test.go index 52a0e1d23e..479d3c1c5e 100644 --- a/codegen/service/convert_test.go +++ b/codegen/service/convert_test.go @@ -14,7 +14,6 @@ import ( "goa.design/goa/v3/codegen" "goa.design/goa/v3/codegen/service/testdata" "goa.design/goa/v3/dsl" - "goa.design/goa/v3/eval" "goa.design/goa/v3/expr" ) @@ -257,30 +256,7 @@ func TestConvertFile(t *testing.T) { } } -// runDSL returns the DSL root resulting from running the given DSL. -func runDSL(t *testing.T, dsl func()) *expr.RootExpr { - // reset all roots and codegen data structures - Services = make(ServicesData) - eval.Reset() - expr.Root = new(expr.RootExpr) - expr.GeneratedResultTypes = new(expr.ResultTypesRoot) - require.NoError(t, eval.Register(expr.Root)) - require.NoError(t, eval.Register(expr.GeneratedResultTypes)) - expr.Root.API = expr.NewAPIExpr("test api", func() {}) - expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()} - - // run DSL (first pass) - require.True(t, eval.Execute(dsl, nil)) - - // run DSL (second pass) - require.NoError(t, eval.RunDSL()) - - // return generated root - return expr.Root -} - // Test fixtures - var obj = &expr.UserTypeExpr{ AttributeExpr: &expr.AttributeExpr{ Type: &expr.Object{ diff --git a/codegen/service/endpoint.go b/codegen/service/endpoint.go index aaefc4bbd2..45d476620e 100644 --- a/codegen/service/endpoint.go +++ b/codegen/service/endpoint.go @@ -25,6 +25,10 @@ type ( ServiceVarName string // Methods lists the endpoint struct methods. Methods []*EndpointMethodData + // HasServerInterceptors indicates if the service has server interceptors. + HasServerInterceptors bool + // HasClientInterceptors indicates if the service has client interceptors. + HasClientInterceptors bool // ClientInitArgs lists the arguments needed to instantiate the client. ClientInitArgs string // Schemes contains the security schemes types used by the @@ -44,6 +48,10 @@ type ( ServiceName string // ServiceVarName is the name of the owner service Go interface. ServiceVarName string + // ServerInterceptors contains the server-side interceptors for this method + ServerInterceptors []*InterceptorData + // ClientInterceptors contains the client-side interceptors for this method + ClientInterceptors []*InterceptorData } ) @@ -122,6 +130,18 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File { Data: m, FuncMap: map[string]any{"payloadVar": payloadVar}, }) + if len(m.ServerInterceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "endpoint-wrapper", + Source: readTemplate("endpoint_wrappers"), + Data: map[string]interface{}{ + "MethodVarName": codegen.Goify(m.Name, true), + "Method": m.Name, + "Service": svc.Name, + "ServerInterceptors": m.ServerInterceptors, + }, + }) + } } } @@ -133,25 +153,45 @@ func endpointData(service *expr.ServiceExpr) *EndpointsData { methods := make([]*EndpointMethodData, len(svc.Methods)) names := make([]string, len(svc.Methods)) for i, m := range svc.Methods { + serverInts, clientInts := buildMethodInterceptors(service.Method(m.Name), svc.Scope) methods[i] = &EndpointMethodData{ - MethodData: m, - ArgName: codegen.Goify(m.VarName, false), - ServiceName: svc.Name, - ServiceVarName: serviceInterfaceName, - ClientVarName: clientStructName, + MethodData: m, + ArgName: codegen.Goify(m.VarName, false), + ServiceName: svc.Name, + ServiceVarName: serviceInterfaceName, + ClientVarName: clientStructName, + ServerInterceptors: serverInts, + ClientInterceptors: clientInts, } names[i] = codegen.Goify(m.VarName, false) } desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, service.Name) + var hasServerInterceptors, hasClientInterceptors bool + for _, m := range methods { + if len(m.ServerInterceptors) > 0 { + hasServerInterceptors = true + if hasClientInterceptors { + break + } + } + if len(m.ClientInterceptors) > 0 { + hasClientInterceptors = true + if hasServerInterceptors { + break + } + } + } return &EndpointsData{ - Name: service.Name, - Description: desc, - VarName: endpointsStructName, - ClientVarName: clientStructName, - ServiceVarName: serviceInterfaceName, - ClientInitArgs: strings.Join(names, ", "), - Methods: methods, - Schemes: svc.Schemes, + Name: service.Name, + Description: desc, + VarName: endpointsStructName, + ClientVarName: clientStructName, + ServiceVarName: serviceInterfaceName, + ClientInitArgs: strings.Join(names, ", "), + Methods: methods, + HasServerInterceptors: hasServerInterceptors, + HasClientInterceptors: hasClientInterceptors, + Schemes: svc.Schemes, } } diff --git a/codegen/service/endpoint_test.go b/codegen/service/endpoint_test.go index 0356bf958e..2132eeeeca 100644 --- a/codegen/service/endpoint_test.go +++ b/codegen/service/endpoint_test.go @@ -34,6 +34,8 @@ func TestEndpoint(t *testing.T) { {"endpoint-streaming-payload-no-result", testdata.StreamingPayloadNoResultMethodDSL, testdata.StreamingPayloadNoResultMethodEndpoint}, {"endpoint-bidirectional-streaming", testdata.BidirectionalStreamingEndpointDSL, testdata.BidirectionalStreamingMethodEndpoint}, {"endpoint-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethodEndpoint}, + {"endpoint-with-server-interceptor", testdata.EndpointWithServerInterceptorDSL, testdata.EndpointWithServerInterceptor}, + {"endpoint-with-multiple-interceptors", testdata.EndpointWithMultipleInterceptorsDSL, testdata.EndpointWithMultipleInterceptors}, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { diff --git a/codegen/service/interceptors.go b/codegen/service/interceptors.go new file mode 100644 index 0000000000..57d734e0c6 --- /dev/null +++ b/codegen/service/interceptors.go @@ -0,0 +1,215 @@ +package service + +import ( + "path/filepath" + + "goa.design/goa/v3/codegen" + "goa.design/goa/v3/expr" +) + +type ( + // ServiceInterceptorData contains all data needed for generating interceptor code + ServiceInterceptorData struct { + Service string + PkgName string + Methods []*MethodInterceptorData + ServerInterceptors []*InterceptorData + ClientInterceptors []*InterceptorData + AllInterceptors []*InterceptorData + HasPrivateImplementationTypes bool + } + + // MethodInterceptorData contains interceptor data for a single method + MethodInterceptorData struct { + Service string + Method string + MethodVarName string + PayloadRef string + ResultRef string + ServerInterceptors []*InterceptorData + ClientInterceptors []*InterceptorData + } + + // InterceptorData describes a single interceptor. + InterceptorData struct { + Name string + UnexportedName string + Description string + PayloadRef string + ResultRef string + ReadPayload []*AttributeData + WritePayload []*AttributeData + ReadResult []*AttributeData + WriteResult []*AttributeData + ServerStreamInputStruct string + ClientStreamInputStruct string + } + + // AttributeData describes a single attribute. + AttributeData struct { + Name string + TypeRef string + FieldPointer bool + } +) + +// InterceptorsFile returns the interceptors file for the given service. +func InterceptorsFile(genpkg string, service *expr.ServiceExpr) *codegen.File { + svc := Services.Get(service.Name) + data := interceptorsData(service) + if len(data.ServerInterceptors) == 0 && len(data.ClientInterceptors) == 0 { + return nil + } + + path := filepath.Join(codegen.Gendir, svc.PathName, "interceptors.go") + sections := []*codegen.SectionTemplate{ + codegen.Header(service.Name+" interceptors", svc.PkgName, []*codegen.ImportSpec{ + {Path: "context"}, + codegen.GoaImport(""), + }), + { + Name: "interceptors", + Source: readTemplate("interceptors"), + Data: data, + }, + } + + return &codegen.File{Path: path, SectionTemplates: sections} +} + +func interceptorsData(service *expr.ServiceExpr) *ServiceInterceptorData { + svc := Services.Get(service.Name) + scope := svc.Scope + + // Build method data first + methods := make([]*MethodInterceptorData, 0, len(service.Methods)) + seenInts := make(map[string]*InterceptorData) + var serviceServerInts, serviceClientInts, allInts []*InterceptorData + var hasTypes bool + + for _, m := range service.Methods { + methodServerInts, methodClientInts := buildMethodInterceptors(m, scope) + if len(methodServerInts) == 0 && len(methodClientInts) == 0 { + continue + } + hasTypes = hasTypes || hasPrivateImplementationTypes(methodServerInts) || hasPrivateImplementationTypes(methodClientInts) + + // Add method data + methods = append(methods, &MethodInterceptorData{ + Service: svc.Name, + Method: m.Name, + MethodVarName: codegen.Goify(m.Name, true), + PayloadRef: scope.GoFullTypeRef(m.Payload, ""), + ResultRef: scope.GoFullTypeRef(m.Result, ""), + ServerInterceptors: methodServerInts, + ClientInterceptors: methodClientInts, + }) + + // Collect unique interceptors + for _, i := range methodServerInts { + if _, ok := seenInts[i.Name]; !ok { + seenInts[i.Name] = i + serviceServerInts = append(serviceServerInts, i) + allInts = append(allInts, i) + } + } + for _, i := range methodClientInts { + if _, ok := seenInts[i.Name]; !ok { + seenInts[i.Name] = i + serviceClientInts = append(serviceClientInts, i) + allInts = append(allInts, i) + } + } + } + + return &ServiceInterceptorData{ + Service: service.Name, + PkgName: svc.PkgName, + Methods: methods, + ServerInterceptors: serviceServerInts, + ClientInterceptors: serviceClientInts, + AllInterceptors: allInts, + HasPrivateImplementationTypes: hasTypes, + } +} + +func buildMethodInterceptors(m *expr.MethodExpr, scope *codegen.NameScope) ([]*InterceptorData, []*InterceptorData) { + svc := Services.Get(m.Service.Name) + methodData := svc.Method(m.Name) + var serverEndpointStruct, clientEndpointStruct string + if methodData.ServerStream != nil { + serverEndpointStruct = methodData.ServerStream.EndpointStruct + } + if methodData.ClientStream != nil { + clientEndpointStruct = methodData.ClientStream.EndpointStruct + } + var hasPrivateImplementationTypes bool + buildInterceptor := func(intr *expr.InterceptorExpr) *InterceptorData { + hasPrivateImplementationTypes = hasPrivateImplementationTypes || + intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil + + return &InterceptorData{ + Name: codegen.Goify(intr.Name, true), + UnexportedName: codegen.Goify(intr.Name, false), + Description: intr.Description, + PayloadRef: methodData.PayloadRef, + ResultRef: methodData.ResultRef, + ServerStreamInputStruct: serverEndpointStruct, + ClientStreamInputStruct: clientEndpointStruct, + ReadPayload: collectAttributes(intr.ReadPayload, m.Payload, scope), + WritePayload: collectAttributes(intr.WritePayload, m.Payload, scope), + ReadResult: collectAttributes(intr.ReadResult, m.Result, scope), + WriteResult: collectAttributes(intr.WriteResult, m.Result, scope), + } + } + + serverInts := make([]*InterceptorData, len(m.ServerInterceptors)) + for i, intr := range m.ServerInterceptors { + serverInts[i] = buildInterceptor(intr) + } + + clientInts := make([]*InterceptorData, len(m.ClientInterceptors)) + for i, intr := range m.ClientInterceptors { + clientInts[i] = buildInterceptor(intr) + } + + return serverInts, clientInts +} + +// hasPrivateImplementationTypes returns true if any of the interceptors have +// private implementation types. +func hasPrivateImplementationTypes(interceptors []*InterceptorData) bool { + for _, intr := range interceptors { + if intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil { + return true + } + } + return false +} + +// collectAttributes builds AttributeData from an AttributeExpr +func collectAttributes(attrNames, parent *expr.AttributeExpr, scope *codegen.NameScope) []*AttributeData { + if attrNames == nil { + return nil + } + + obj := expr.AsObject(attrNames.Type) + if obj == nil { + return nil + } + + data := make([]*AttributeData, len(*obj)) + for i, nat := range *obj { + parentAttr := parent.Find(nat.Name) + if parentAttr == nil { + continue + } + + data[i] = &AttributeData{ + Name: codegen.Goify(nat.Name, true), + TypeRef: scope.GoTypeRef(parentAttr), + FieldPointer: parent.IsPrimitivePointer(nat.Name, true), + } + } + return data +} diff --git a/codegen/service/interceptors_test.go b/codegen/service/interceptors_test.go new file mode 100644 index 0000000000..0817ac2b31 --- /dev/null +++ b/codegen/service/interceptors_test.go @@ -0,0 +1,209 @@ +package service + +import ( + "bytes" + "flag" + "go/format" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "goa.design/goa/v3/codegen" + "goa.design/goa/v3/codegen/service/testdata" + "goa.design/goa/v3/expr" +) + +var updateGolden = false + +func init() { + flag.BoolVar(&updateGolden, "w", false, "update golden files") +} + +func TestInterceptors(t *testing.T) { + cases := []struct { + Name string + DSL func() + }{ + {"no-interceptors", testdata.NoInterceptorsDSL}, + {"single-api-server-interceptor", testdata.SingleAPIServerInterceptorDSL}, + {"single-service-server-interceptor", testdata.SingleServiceServerInterceptorDSL}, + {"single-method-server-interceptor", testdata.SingleMethodServerInterceptorDSL}, + {"single-client-interceptor", testdata.SingleClientInterceptorDSL}, + {"multiple-interceptors", testdata.MultipleInterceptorsDSL}, + {"interceptor-with-read-payload", testdata.InterceptorWithReadPayloadDSL}, + {"interceptor-with-write-payload", testdata.InterceptorWithWritePayloadDSL}, + {"interceptor-with-read-write-payload", testdata.InterceptorWithReadWritePayloadDSL}, + {"interceptor-with-read-result", testdata.InterceptorWithReadResultDSL}, + {"interceptor-with-write-result", testdata.InterceptorWithWriteResultDSL}, + {"interceptor-with-read-write-result", testdata.InterceptorWithReadWriteResultDSL}, + {"streaming-interceptors", testdata.StreamingInterceptorsDSL}, + {"streaming-interceptors-with-read-payload", testdata.StreamingInterceptorsWithReadPayloadDSL}, + {"streaming-interceptors-with-read-result", testdata.StreamingInterceptorsWithReadResultDSL}, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + root := runDSL(t, c.DSL) + require.Len(t, root.Services, 1) + + fs := InterceptorsFile("goa.design/goa/example", root.Services[0]) + + if c.Name == "no-interceptors" { + assert.Nil(t, fs) + return + } + + require.NotNil(t, fs) + + buf := new(bytes.Buffer) + for _, s := range fs.SectionTemplates[1:] { + require.NoError(t, s.Write(buf)) + } + bs, err := format.Source(buf.Bytes()) + require.NoError(t, err, buf.String()) + code := strings.ReplaceAll(string(bs), "\r\n", "\n") + + golden := filepath.Join("testdata", "interceptors", c.Name+".golden") + compareOrUpdateGolden(t, code, golden) + }) + } +} + +func TestInvalidInterceptors(t *testing.T) { + cases := []struct { + Name string + DSL func() + ErrContains string + }{ + { + Name: "streaming-result-interceptor", + DSL: testdata.StreamingResultInterceptorDSL, + ErrContains: "cannot be applied because the method result is streaming", + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + _, err := runDSLWithError(t, c.DSL) + require.Error(t, err) + assert.Contains(t, err.Error(), c.ErrContains) + }) + } +} + +func TestCollectAttributes(t *testing.T) { + cases := []struct { + name string + attrNames *expr.AttributeExpr + parent *expr.AttributeExpr + want []*AttributeData + }{ + { + name: "nil-attributes", + attrNames: nil, + parent: &expr.AttributeExpr{Type: &expr.Object{}}, + want: nil, + }, + { + name: "non-object-attributes", + attrNames: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}, + parent: &expr.AttributeExpr{Type: &expr.Object{}}, + want: nil, + }, + { + name: "simple-string-attribute", + attrNames: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + }, + }, + parent: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + }, + Validation: &expr.ValidationExpr{Required: []string{"name"}}, + }, + want: []*AttributeData{ + {Name: "Name", TypeRef: "string", FieldPointer: false}, + }, + }, + { + name: "pointer-primitive", + attrNames: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind)}}, + }, + }, + parent: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind), Meta: map[string][]string{"struct:field:pointer": {"true"}}}}, + }, + }, + want: []*AttributeData{ + {Name: "Age", TypeRef: "int", FieldPointer: true}, + }, + }, + { + name: "multiple-attributes", + attrNames: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + {Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind)}}, + }, + }, + parent: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + {Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind), Meta: map[string][]string{"struct:field:pointer": {"true"}}}}, + }, + Validation: &expr.ValidationExpr{Required: []string{"name"}}, + }, + want: []*AttributeData{ + {Name: "Name", TypeRef: "string", FieldPointer: false}, + {Name: "Age", TypeRef: "int", FieldPointer: true}, + }, + }, + { + name: "attribute-not-in-parent", + attrNames: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "missing", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + }, + }, + parent: &expr.AttributeExpr{ + Type: &expr.Object{ + {Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}}, + }, + Validation: &expr.ValidationExpr{Required: []string{"name"}}, + }, + want: []*AttributeData{nil}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + scope := codegen.NewNameScope() + got := collectAttributes(tc.attrNames, tc.parent, scope) + assert.Equal(t, tc.want, got) + }) + } +} + +func compareOrUpdateGolden(t *testing.T, code, golden string) { + t.Helper() + if updateGolden { + require.NoError(t, os.MkdirAll(filepath.Dir(golden), 0750)) + require.NoError(t, os.WriteFile(golden, []byte(code), 0640)) + return + } + data, err := os.ReadFile(golden) + require.NoError(t, err) + if runtime.GOOS == "windows" { + data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + } + assert.Equal(t, string(data), code) +} diff --git a/codegen/service/service.go b/codegen/service/service.go index 8d920c7854..a6c45448d1 100644 --- a/codegen/service/service.go +++ b/codegen/service/service.go @@ -195,6 +195,11 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s } files := []*codegen.File{{Path: svcPath, SectionTemplates: sections}} + // interceptor.go + if file := InterceptorsFile(genpkg, service); file != nil { + files = append(files, file) + } + // user types paths := make([]string, len(typeDefSections)) i := 0 diff --git a/codegen/service/templates/client_wrappers.go.tpl b/codegen/service/templates/client_wrappers.go.tpl new file mode 100644 index 0000000000..94364c38cd --- /dev/null +++ b/codegen/service/templates/client_wrappers.go.tpl @@ -0,0 +1,29 @@ +{{ comment (printf "Wrap%sClientEndpoint wraps the %s endpoint with the client interceptors defined in the design." .MethodVarName .Method) }} +func Wrap{{ .MethodVarName }}ClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + {{- range .ClientInterceptors }} + endpoint = wrapClient{{ .Name }}(endpoint, i, "{{ $.Method }}") + {{- end }} + return endpoint +} + +{{- range .ClientInterceptors }} +{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .Name) }} +func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &{{ .Name }}Info{ + Service: "{{ $.Service }}", + Method: method, + Endpoint: endpoint, + {{- if .ClientStreamInputStruct }} + RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.{{ .Name }}(ctx, info, next) + } +} +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/endpoint_wrappers.go.tpl b/codegen/service/templates/endpoint_wrappers.go.tpl new file mode 100644 index 0000000000..bae0b9fa25 --- /dev/null +++ b/codegen/service/templates/endpoint_wrappers.go.tpl @@ -0,0 +1,30 @@ +{{ comment (printf "Wrap%sEndpoint wraps the %s endpoint with the server-side interceptors defined in the design." .MethodVarName .Method) }} +func Wrap{{ .MethodVarName }}Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + {{- range .ServerInterceptors }} + endpoint = wrap{{ .Name }}(endpoint, i, "{{ $.Method }}") + {{- end }} + return endpoint +} + +{{- range .ServerInterceptors }} +{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .Name) }} +func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &{{ .Name }}Info{ + Service: "{{ $.Service }}", + Method: method, + Endpoint: endpoint, + {{- if .ServerStreamInputStruct }} + RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.{{ .Name }}(ctx, info, next) + } +} + +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/interceptors.go.tpl b/codegen/service/templates/interceptors.go.tpl new file mode 100644 index 0000000000..46dfceedb0 --- /dev/null +++ b/codegen/service/templates/interceptors.go.tpl @@ -0,0 +1,153 @@ +{{- if .ServerInterceptors -}} +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { +{{- range .ServerInterceptors }} + {{ comment .Description }} + {{ .Name }}(context.Context, *{{ .Name }}Info, goa.NextFunc) (any, error) +{{- end }} +} +{{- end }} + +{{- if .ClientInterceptors -}} +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server (request interceptors) or after the response is decoded +// and before the result is returned to the client (response interceptors). +type ClientInterceptors interface { +{{- range .ClientInterceptors }} + {{ comment .Description }} + {{ .Name }}(context.Context, *{{ .Name }}Info, goa.NextFunc) (any, error) +{{- end }} +} +{{- end }} + +// Access interfaces for interceptor payloads and results +type ( +{{- range .AllInterceptors }} + // {{ .Name }}Info provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + {{ .Name }}Info goa.InterceptorInfo + {{- if or .ReadPayload .WritePayload }} + + // {{ .Name }}PayloadAccess provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + {{ .Name }}PayloadAccess interface { + {{- range .ReadPayload }} + {{ .Name }}() {{ .TypeRef }} + {{- end }} + {{- range .WritePayload }} + Set{{ .Name }}({{ .TypeRef }}) + {{- end }} + } + {{- end }} + {{- if or .ReadResult .WriteResult }} + + // {{ .Name }}ResultAccess provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + {{ .Name }}ResultAccess interface { + {{- range .ReadResult }} + {{ .Name }}() {{ .TypeRef }} + {{- end }} + {{- range .WriteResult }} + Set{{ .Name }}({{ .TypeRef }}) + {{- end }} + } + {{- end }} +{{- end }} +) + +{{- if .HasPrivateImplementationTypes }} + +// Private implementation types +type ( + {{- range .AllInterceptors }} + {{- if or .ReadPayload .WritePayload }} + {{ .UnexportedName }}PayloadAccess struct { + payload {{ .PayloadRef }} + } + {{- end }} + + {{- if or .ReadResult .WriteResult }} + {{ .UnexportedName }}ResultAccess struct { + result {{ .ResultRef }} + } + {{- end }} + {{- end }} +) + +// Public accessor methods for Info types +{{- range .AllInterceptors }} + {{- if or .ReadPayload .WritePayload }} +// Payload returns a type-safe accessor for the method payload. +func (info *{{ .Name }}Info) Payload() {{ .Name }}PayloadAccess { + return &{{ .UnexportedName }}PayloadAccess{payload: info.RawPayload.({{ .PayloadRef }})} +} + {{- end }} + + {{- if or .ReadResult .WriteResult }} +// Result returns a type-safe accessor for the method result. +func (info *{{ .Name }}Info) Result(res any) {{ .Name }}ResultAccess { + return &{{ .UnexportedName }}ResultAccess{result: res.({{ .ResultRef }})} +} + {{- end }} +{{- end }} + +// Private implementation methods +{{- range .AllInterceptors }} + {{- $interceptor := . }} + {{- range .ReadPayload }} +func (p *{{ $interceptor.UnexportedName }}PayloadAccess) {{ .Name }}() {{ .TypeRef }} { + {{- if .FieldPointer }} + if p.payload.{{ .Name }} == nil { + var zero {{ .TypeRef }} + return zero + } + return *p.payload.{{ .Name }} + {{- else }} + return p.payload.{{ .Name }} + {{- end }} +} + {{- end }} + + {{- range .WritePayload }} +func (p *{{ $interceptor.UnexportedName }}PayloadAccess) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .FieldPointer }} + p.payload.{{ .Name }} = &v + {{- else }} + p.payload.{{ .Name }} = v + {{- end }} +} + {{- end }} + + {{- range .ReadResult }} +func (r *{{ $interceptor.UnexportedName }}ResultAccess) {{ .Name }}() {{ .TypeRef }} { + {{- if .FieldPointer }} + if r.result.{{ .Name }} == nil { + var zero {{ .TypeRef }} + return zero + } + return *r.result.{{ .Name }} + {{- else }} + return r.result.{{ .Name }} + {{- end }} +} + {{- end }} + + {{- range .WriteResult }} +func (r *{{ $interceptor.UnexportedName }}ResultAccess) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .FieldPointer }} + r.result.{{ .Name }} = &v + {{- else }} + r.result.{{ .Name }} = v + {{- end }} +} + {{- end }} +{{- end }} +{{- end }} + + diff --git a/codegen/service/templates/service_client_init.go.tpl b/codegen/service/templates/service_client_init.go.tpl index 3f2302c524..20621066a3 100644 --- a/codegen/service/templates/service_client_init.go.tpl +++ b/codegen/service/templates/service_client_init.go.tpl @@ -1,8 +1,8 @@ {{ printf "New%s initializes a %q service client given the endpoints." .ClientVarName .Name | comment }} -func New{{ .ClientVarName }}({{ .ClientInitArgs }} goa.Endpoint) *{{ .ClientVarName }} { - return &{{ .ClientVarName }}{ -{{- range .Methods }} - {{ .VarName }}Endpoint: {{ .ArgName }}, -{{- end }} - } +func New{{ .ClientVarName }}({{ .ClientInitArgs }} goa.Endpoint{{ if .HasClientInterceptors }}, ci ClientInterceptors{{ end }}) *{{ .ClientVarName }} { + return &{{ .ClientVarName }}{ + {{- range .Methods }} + {{ .VarName }}Endpoint: {{ if .ClientInterceptors }}Wrap{{ .VarName }}ClientEndpoint({{ end }}{{ .ArgName }}{{ if .ClientInterceptors }}, ci){{ end }}, + {{- end }} + } } diff --git a/codegen/service/templates/service_endpoints_init.go.tpl b/codegen/service/templates/service_endpoints_init.go.tpl index 64a9803a6b..3f46fc4d88 100644 --- a/codegen/service/templates/service_endpoints_init.go.tpl +++ b/codegen/service/templates/service_endpoints_init.go.tpl @@ -1,14 +1,25 @@ - {{ printf "New%s wraps the methods of the %q service with endpoints." .VarName .Name | comment }} -func New{{ .VarName }}(s {{ .ServiceVarName }}) *{{ .VarName }} { +func New{{ .VarName }}(s {{ .ServiceVarName }}{{ if .HasServerInterceptors }}, si ServerInterceptors{{ end }}) *{{ .VarName }} { {{- if .Schemes }} // Casting service to Auther interface a := s.(Auther) {{- end }} +{{- if .HasServerInterceptors }} + endpoints := &{{ .VarName }}{ +{{- else }} return &{{ .VarName }}{ +{{- end }} {{- range .Methods }} {{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes }}, a.{{ .Type }}Auth{{ end }}), {{- end }} } +{{- if .HasServerInterceptors }} + {{- range .Methods }} + {{- if .ServerInterceptors }} + endpoints.{{ .VarName }} = Wrap{{ .VarName }}Endpoint(endpoints.{{ .VarName }}, si) + {{- end }} + {{- end }} + return endpoints +{{- end }} } \ No newline at end of file diff --git a/codegen/service/templates/service_interceptor.go.tpl b/codegen/service/templates/service_interceptor.go.tpl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/codegen/service/testdata/client_code.go b/codegen/service/testdata/client_code.go index 2b2356134d..96f5a7b6a0 100644 --- a/codegen/service/testdata/client_code.go +++ b/codegen/service/testdata/client_code.go @@ -283,3 +283,50 @@ func (c *Client) BidirectionalStreamingNoPayloadMethod(ctx context.Context) (res return ires.(BidirectionalStreamingNoPayloadMethodClientStream), nil } ` + +const InterceptorClient = `// Client is the "ServiceWithClientInterceptor" service client. +type Client struct { + MethodEndpoint goa.Endpoint +} + +// NewClient initializes a "ServiceWithClientInterceptor" service client given +// the endpoints. +func NewClient(method goa.Endpoint, ci ClientInterceptors) *Client { + return &Client{ + MethodEndpoint: WrapMethodClientEndpoint(method, ci), + } +} + +// Method calls the "Method" endpoint of the "ServiceWithClientInterceptor" +// service. +func (c *Client) Method(ctx context.Context, p string) (res string, err error) { + var ires any + ires, err = c.MethodEndpoint(ctx, p) + if err != nil { + return + } + return ires.(string), nil +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientTracing(endpoint, i, "Method") + return endpoint +} + +// wrapClientTracing applies the Tracing interceptor to endpoints. +func wrapClientTracing(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &TracingInfo{ + Service: "ServiceWithClientInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.Tracing(ctx, info, next) + } +}` diff --git a/codegen/service/testdata/endpoint_code.go b/codegen/service/testdata/endpoint_code.go index 978ff5e5f7..27967a6e08 100644 --- a/codegen/service/testdata/endpoint_code.go +++ b/codegen/service/testdata/endpoint_code.go @@ -513,3 +513,184 @@ func NewBidirectionalStreamingNoPayloadMethodEndpoint(s Service) goa.Endpoint { } } ` + +var EndpointWithServerInterceptor = `// Endpoints wraps the "ServiceWithServerInterceptor" service endpoints. +type Endpoints struct { + Method goa.Endpoint +} + +// NewEndpoints wraps the methods of the "ServiceWithServerInterceptor" service +// with endpoints. +func NewEndpoints(s Service, si ServerInterceptors) *Endpoints { + endpoints := &Endpoints{ + Method: NewMethodEndpoint(s), + } + endpoints.Method = WrapMethodEndpoint(endpoints.Method, si) + return endpoints +} + +// Use applies the given middleware to all the "ServiceWithServerInterceptor" +// service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Method = m(e.Method) +} + +// NewMethodEndpoint returns an endpoint function that calls the method +// "Method" of service "ServiceWithServerInterceptor". +func NewMethodEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(string) + return s.Method(ctx, p) + } +} + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} + +// wrapLogging applies the Logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "ServiceWithServerInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.Logging(ctx, info, next) + } +}` + +var EndpointWithMultipleInterceptors = `// Endpoints wraps the "ServiceWithMultipleInterceptors" service endpoints. +type Endpoints struct { + Method goa.Endpoint +} + +// NewEndpoints wraps the methods of the "ServiceWithMultipleInterceptors" +// service with endpoints. +func NewEndpoints(s Service, si ServerInterceptors) *Endpoints { + endpoints := &Endpoints{ + Method: NewMethodEndpoint(s), + } + endpoints.Method = WrapMethodEndpoint(endpoints.Method, si) + return endpoints +} + +// Use applies the given middleware to all the +// "ServiceWithMultipleInterceptors" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Method = m(e.Method) +} + +// NewMethodEndpoint returns an endpoint function that calls the method +// "Method" of service "ServiceWithMultipleInterceptors". +func NewMethodEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(string) + return s.Method(ctx, p) + } +} + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + endpoint = wrapMetrics(endpoint, i, "Method") + return endpoint +} + +// wrapLogging applies the Logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "ServiceWithMultipleInterceptors", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.Logging(ctx, info, next) + } +} + +// wrapMetrics applies the Metrics interceptor to endpoints. +func wrapMetrics(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &MetricsInfo{ + Service: "ServiceWithMultipleInterceptors", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.Metrics(ctx, info, next) + } +}` + +var EndpointStreamingWithInterceptor = `// Endpoints wraps the "ServiceStreamingWithInterceptor" service endpoints. +type Endpoints struct { + Method goa.Endpoint +} + +// MethodEndpointInput holds both the payload and the server stream of the +// "Method" method. +type MethodEndpointInput struct { + // Stream is the server stream used by the "Method" method to send data. + Stream MethodServerStream +} + +// NewEndpoints wraps the methods of the "ServiceStreamingWithInterceptor" service +// with endpoints. +func NewEndpoints(s Service, i ServerInterceptors) *Endpoints { + return &Endpoints{ + Method: WrapMethodEndpoint(NewMethodEndpoint(s), i), + } +} + +// Use applies the given middleware to all the "ServiceStreamingWithInterceptor" +// service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Method = m(e.Method) +} + +// NewMethodEndpoint returns an endpoint function that calls the method "Method" +// of service "ServiceStreamingWithInterceptor". +func NewMethodEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + ep := req.(*MethodEndpointInput) + return nil, s.Method(ctx, ep.Stream) + } +} + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} + +// wrapLogging applies the Logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "ServiceStreamingWithInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req.(*MethodEndpointInput).Payload, + } + next := func(ctx context.Context) (any, error) { + return endpoint(ctx, req) + } + return i.Logging(ctx, info, next) + } +}` diff --git a/codegen/service/testdata/endpoint_dsls.go b/codegen/service/testdata/endpoint_dsls.go index 4ca2c7b435..de3a220401 100644 --- a/codegen/service/testdata/endpoint_dsls.go +++ b/codegen/service/testdata/endpoint_dsls.go @@ -181,3 +181,47 @@ var BidirectionalStreamingEndpointDSL = func() { }) }) } + +var EndpointWithServerInterceptorDSL = func() { + Interceptor("logging") + Service("ServiceWithServerInterceptor", func() { + Method("Method", func() { + ServerInterceptor("logging") + Payload(String) + Result(String) + HTTP(func() { + POST("/") + }) + }) + }) +} + +var EndpointWithMultipleInterceptorsDSL = func() { + Interceptor("logging") + Interceptor("metrics") + Service("ServiceWithMultipleInterceptors", func() { + Method("Method", func() { + ServerInterceptor("logging") + ServerInterceptor("metrics") + Payload(String) + Result(String) + HTTP(func() { + POST("/") + }) + }) + }) +} + +var EndpointStreamingWithInterceptorDSL = func() { + Interceptor("logging") + Service("ServiceStreamingWithInterceptor", func() { + Method("Method", func() { + ServerInterceptor("logging") + StreamingPayload(String) + StreamingResult(String) + HTTP(func() { + GET("/") + }) + }) + }) +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload.golden new file mode 100644 index 0000000000..eb5152b895 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload.golden @@ -0,0 +1,41 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // ValidationInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + ValidationInfo goa.InterceptorInfo + + // ValidationPayloadAccess provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayloadAccess interface { + Name() string + } +) + +// Private implementation types +type ( + validationPayloadAccess struct { + payload *MethodPayload + } +) + +// Public accessor methods for Info types +// Payload returns a type-safe accessor for the method payload. +func (info *ValidationInfo) Payload() ValidationPayloadAccess { + return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods +func (p *validationPayloadAccess) Name() string { + return p.payload.Name +} + + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result.golden new file mode 100644 index 0000000000..254d1ddaf0 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result.golden @@ -0,0 +1,41 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // CachingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + CachingInfo goa.InterceptorInfo + + // CachingResultAccess provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResultAccess interface { + Data() string + } +) + +// Private implementation types +type ( + cachingResultAccess struct { + result *MethodResult + } +) + +// Public accessor methods for Info types +// Result returns a type-safe accessor for the method result. +func (info *CachingInfo) Result(res any) CachingResultAccess { + return &cachingResultAccess{result: res.(*MethodResult)} +} + +// Private implementation methods +func (r *cachingResultAccess) Data() string { + return r.result.Data +} + + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden new file mode 100644 index 0000000000..07d0f938cb --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden @@ -0,0 +1,45 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // ValidationInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + ValidationInfo goa.InterceptorInfo + + // ValidationPayloadAccess provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayloadAccess interface { + Name() string + SetName(string) + } +) + +// Private implementation types +type ( + validationPayloadAccess struct { + payload *MethodPayload + } +) + +// Public accessor methods for Info types +// Payload returns a type-safe accessor for the method payload. +func (info *ValidationInfo) Payload() ValidationPayloadAccess { + return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods +func (p *validationPayloadAccess) Name() string { + return p.payload.Name +} +func (p *validationPayloadAccess) SetName(v string) { + p.payload.Name = v +} + + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden new file mode 100644 index 0000000000..202260177d --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden @@ -0,0 +1,45 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // CachingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + CachingInfo goa.InterceptorInfo + + // CachingResultAccess provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResultAccess interface { + Data() string + SetData(string) + } +) + +// Private implementation types +type ( + cachingResultAccess struct { + result *MethodResult + } +) + +// Public accessor methods for Info types +// Result returns a type-safe accessor for the method result. +func (info *CachingInfo) Result(res any) CachingResultAccess { + return &cachingResultAccess{result: res.(*MethodResult)} +} + +// Private implementation methods +func (r *cachingResultAccess) Data() string { + return r.result.Data +} +func (r *cachingResultAccess) SetData(v string) { + r.result.Data = v +} + + diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload.golden new file mode 100644 index 0000000000..7f0eabeb04 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload.golden @@ -0,0 +1,41 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // ValidationInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + ValidationInfo goa.InterceptorInfo + + // ValidationPayloadAccess provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayloadAccess interface { + SetName(string) + } +) + +// Private implementation types +type ( + validationPayloadAccess struct { + payload *MethodPayload + } +) + +// Public accessor methods for Info types +// Payload returns a type-safe accessor for the method payload. +func (info *ValidationInfo) Payload() ValidationPayloadAccess { + return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods +func (p *validationPayloadAccess) SetName(v string) { + p.payload.Name = v +} + + diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result.golden new file mode 100644 index 0000000000..362ae77d6a --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result.golden @@ -0,0 +1,41 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // CachingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + CachingInfo goa.InterceptorInfo + + // CachingResultAccess provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResultAccess interface { + SetData(string) + } +) + +// Private implementation types +type ( + cachingResultAccess struct { + result *MethodResult + } +) + +// Public accessor methods for Info types +// Result returns a type-safe accessor for the method result. +func (info *CachingInfo) Result(res any) CachingResultAccess { + return &cachingResultAccess{result: res.(*MethodResult)} +} + +// Private implementation methods +func (r *cachingResultAccess) SetData(v string) { + r.result.Data = v +} + + diff --git a/codegen/service/testdata/interceptors/multiple-interceptors.golden b/codegen/service/testdata/interceptors/multiple-interceptors.golden new file mode 100644 index 0000000000..111d148050 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors.golden @@ -0,0 +1,30 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) + + Tracing(context.Context, *TracingInfo, goa.NextFunc) (any, error) +} // ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server (request interceptors) or after the response is decoded +// and before the result is returned to the client (response interceptors). +type ClientInterceptors interface { + Metrics(context.Context, *MetricsInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo + // TracingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + TracingInfo goa.InterceptorInfo + // MetricsInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + MetricsInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor.golden new file mode 100644 index 0000000000..ae003b0548 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor.golden @@ -0,0 +1,16 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors/single-client-interceptor.golden b/codegen/service/testdata/interceptors/single-client-interceptor.golden new file mode 100644 index 0000000000..0fc4e0f1a9 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-client-interceptor.golden @@ -0,0 +1,16 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server (request interceptors) or after the response is decoded +// and before the result is returned to the client (response interceptors). +type ClientInterceptors interface { + Tracing(context.Context, *TracingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // TracingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + TracingInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor.golden new file mode 100644 index 0000000000..ae003b0548 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor.golden @@ -0,0 +1,16 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor.golden new file mode 100644 index 0000000000..ae003b0548 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor.golden @@ -0,0 +1,16 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden new file mode 100644 index 0000000000..68f1cf20cb --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden @@ -0,0 +1,45 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo + + // LoggingPayloadAccess provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + LoggingPayloadAccess interface { + Initial() string + } +) + +// Private implementation types +type ( + loggingPayloadAccess struct { + payload *MethodPayload + } +) + +// Public accessor methods for Info types +// Payload returns a type-safe accessor for the method payload. +func (info *LoggingInfo) Payload() LoggingPayloadAccess { + return &loggingPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods +func (p *loggingPayloadAccess) Initial() string { + if p.payload.Initial == nil { + var zero string + return zero + } + return *p.payload.Initial +} + + diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden new file mode 100644 index 0000000000..4dec14cb90 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden @@ -0,0 +1,45 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo + + // LoggingResultAccess provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + LoggingResultAccess interface { + Data() string + } +) + +// Private implementation types +type ( + loggingResultAccess struct { + result *MethodResult + } +) + +// Public accessor methods for Info types +// Result returns a type-safe accessor for the method result. +func (info *LoggingInfo) Result(res any) LoggingResultAccess { + return &loggingResultAccess{result: res.(*MethodResult)} +} + +// Private implementation methods +func (r *loggingResultAccess) Data() string { + if r.result.Data == nil { + var zero string + return zero + } + return *r.result.Data +} + + diff --git a/codegen/service/testdata/interceptors/streaming-interceptors.golden b/codegen/service/testdata/interceptors/streaming-interceptors.golden new file mode 100644 index 0000000000..ae003b0548 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors.golden @@ -0,0 +1,16 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the payload +// is sent to the service (request interceptors) or after the service returns and +// before the response is encoded (response interceptors). +type ServerInterceptors interface { + Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + + diff --git a/codegen/service/testdata/interceptors_dsls.go b/codegen/service/testdata/interceptors_dsls.go new file mode 100644 index 0000000000..dead879c9f --- /dev/null +++ b/codegen/service/testdata/interceptors_dsls.go @@ -0,0 +1,297 @@ +package testdata + +import ( + . "goa.design/goa/v3/dsl" +) + +var NoInterceptorsDSL = func() { + Service("NoInterceptors", func() { + Method("Method", func() { + HTTP(func() { GET("/") }) + }) + }) +} + +var SingleAPIServerInterceptorDSL = func() { + Interceptor("logging") + API("SingleAPIServerInterceptor", func() { + ServerInterceptor("logging") + }) + Service("SingleAPIServerInterceptor", func() { + Method("Method", func() { + HTTP(func() { GET("/1") }) + }) + Method("Method2", func() { + HTTP(func() { GET("/2") }) + }) + }) +} + +var SingleServiceServerInterceptorDSL = func() { + Interceptor("logging") + Service("SingleServerInterceptor", func() { + ServerInterceptor("logging") + Method("Method", func() { + HTTP(func() { + GET("/1") + }) + }) + Method("Method2", func() { + HTTP(func() { + GET("/2") + }) + }) + }) +} + +var SingleMethodServerInterceptorDSL = func() { + Interceptor("logging") + Service("SingleMethodServerInterceptor", func() { + Method("Method", func() { + ServerInterceptor("logging") + HTTP(func() { GET("/1") }) + }) + Method("Method2", func() { + HTTP(func() { GET("/2") }) + }) + }) +} + +var SingleClientInterceptorDSL = func() { + Interceptor("tracing") + Service("SingleClientInterceptor", func() { + ClientInterceptor("tracing") + Method("Method", func() { + Payload(func() { + Attribute("id", Int) + }) + Result(func() { + Attribute("value", String) + }) + HTTP(func() { GET("/") }) + }) + }) +} + +var MultipleInterceptorsDSL = func() { + Interceptor("logging") + Interceptor("tracing") + Interceptor("metrics") + Service("MultipleInterceptors", func() { + ServerInterceptor("logging") + ServerInterceptor("tracing") + ClientInterceptor("metrics") + Method("Method", func() { + Payload(func() { + Attribute("query", String) + }) + Result(func() { + Attribute("data", String) + }) + HTTP(func() { GET("/") }) + }) + }) +} + +var InterceptorWithReadPayloadDSL = func() { + Interceptor("validation", func() { + ReadPayload(func() { + Attribute("name") + }) + }) + Service("InterceptorWithReadPayload", func() { + ServerInterceptor("validation") + ClientInterceptor("validation") + Method("Method", func() { + Payload(func() { + Attribute("name", String) + Required("name") + }) + HTTP(func() { POST("/") }) + }) + }) +} + +var InterceptorWithWritePayloadDSL = func() { + Interceptor("validation", func() { + WritePayload(func() { + Attribute("name") + }) + }) + Service("InterceptorWithWritePayload", func() { + ServerInterceptor("validation") + ClientInterceptor("validation") + Method("Method", func() { + Payload(func() { + Attribute("name", String) + Required("name") + }) + HTTP(func() { POST("/") }) + }) + }) +} + +var InterceptorWithReadWritePayloadDSL = func() { + Interceptor("validation", func() { + ReadPayload(func() { + Attribute("name") + }) + WritePayload(func() { + Attribute("name") + }) + }) + Service("InterceptorWithReadWritePayload", func() { + ServerInterceptor("validation") + ClientInterceptor("validation") + Method("Method", func() { + Payload(func() { + Attribute("name", String) + Required("name") + }) + HTTP(func() { POST("/") }) + }) + }) +} + +var InterceptorWithReadResultDSL = func() { + Interceptor("caching", func() { + ReadResult(func() { + Attribute("data") + }) + }) + Service("InterceptorWithReadResult", func() { + ServerInterceptor("caching") + ClientInterceptor("caching") + Method("Method", func() { + Result(func() { + Attribute("data", String) + Required("data") + }) + HTTP(func() { GET("/") }) + }) + }) +} + +var InterceptorWithWriteResultDSL = func() { + Interceptor("caching", func() { + WriteResult(func() { + Attribute("data") + }) + }) + Service("InterceptorWithWriteResult", func() { + ServerInterceptor("caching") + ClientInterceptor("caching") + Method("Method", func() { + Result(func() { + Attribute("data", String) + Required("data") + }) + HTTP(func() { GET("/") }) + }) + }) +} + +var InterceptorWithReadWriteResultDSL = func() { + Interceptor("caching", func() { + ReadResult(func() { + Attribute("data") + }) + WriteResult(func() { + Attribute("data") + }) + }) + Service("InterceptorWithReadWriteResult", func() { + ServerInterceptor("caching") + ClientInterceptor("caching") + Method("Method", func() { + Result(func() { + Attribute("data", String) + Required("data") + }) + HTTP(func() { GET("/") }) + }) + }) +} + +var StreamingInterceptorsDSL = func() { + Interceptor("logging") + Service("StreamingInterceptors", func() { + ServerInterceptor("logging") + Method("Method", func() { + StreamingPayload(func() { + Attribute("chunk", String) + }) + StreamingResult(func() { + Attribute("data", String) + }) + HTTP(func() { GET("/stream") }) + }) + }) +} + +var StreamingInterceptorsWithReadPayloadDSL = func() { + Interceptor("logging", func() { + ReadPayload(func() { + Attribute("initial") + }) + }) + Service("StreamingInterceptorsWithReadPayload", func() { + ServerInterceptor("logging") + Method("Method", func() { + Payload(func() { + Attribute("initial", String) + }) + StreamingPayload(func() { + Attribute("chunk", String) + }) + HTTP(func() { + Header("initial") + GET("/stream") + }) + }) + }) +} + +var StreamingInterceptorsWithReadResultDSL = func() { + Interceptor("logging", func() { + ReadResult(func() { + Attribute("data") + }) + }) + Service("StreamingInterceptorsWithReadResult", func() { + ServerInterceptor("logging") + Method("Method", func() { + Payload(func() { + Attribute("initial", String) + }) + StreamingPayload(func() { + Attribute("chunk", String) + }) + Result(func() { + Attribute("data", String) + }) + HTTP(func() { + Header("initial") + GET("/stream") + }) + }) + }) +} + +// Invalid DSL +var StreamingResultInterceptorDSL = func() { + Interceptor("logging", func() { + ReadResult(func() { + Attribute("data") + }) + }) + Service("StreamingResultInterceptor", func() { + ServerInterceptor("logging") + Method("Method", func() { + StreamingResult(func() { + Attribute("data", String) + }) + HTTP(func() { GET("/stream") }) + }) + }) +} diff --git a/codegen/service/testdata/service_dsls.go b/codegen/service/testdata/service_dsls.go index 5fd9b06b2c..76f7f917b8 100644 --- a/codegen/service/testdata/service_dsls.go +++ b/codegen/service/testdata/service_dsls.go @@ -967,3 +967,17 @@ var PkgPathPayloadAttributeDSL = func() { }) }) } + +var EndpointWithClientInterceptorDSL = func() { + Interceptor("tracing") + Service("ServiceWithClientInterceptor", func() { + Method("Method", func() { + ClientInterceptor("tracing") + Payload(String) + Result(String) + HTTP(func() { + POST("/") + }) + }) + }) +} diff --git a/codegen/service/testing.go b/codegen/service/testing.go new file mode 100644 index 0000000000..28243ecfac --- /dev/null +++ b/codegen/service/testing.go @@ -0,0 +1,42 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +// initDSL initializes the DSL environment and returns the root. +func initDSL(t *testing.T) *expr.RootExpr { + // reset all roots and codegen data structures + Services = make(ServicesData) + eval.Reset() + expr.Root = new(expr.RootExpr) + expr.GeneratedResultTypes = new(expr.ResultTypesRoot) + expr.Root.API = expr.NewAPIExpr("test api", func() {}) + expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()} + root := expr.Root + require.NoError(t, eval.Register(root)) + require.NoError(t, eval.Register(expr.GeneratedResultTypes)) + return root +} + +// runDSL returns the DSL root resulting from running the given DSL. +func runDSL(t *testing.T, dsl func()) *expr.RootExpr { + root := initDSL(t) + require.True(t, eval.Execute(dsl, nil)) + require.NoError(t, eval.RunDSL()) + return root +} + +// runDSLWithError returns the DSL root and error from running the given DSL. +func runDSLWithError(t *testing.T, dsl func()) (*expr.RootExpr, error) { + root := initDSL(t) + require.True(t, eval.Execute(dsl, nil)) + err := eval.RunDSL() + require.Error(t, err) + return root, err +} diff --git a/dsl/description.go b/dsl/description.go index 931c4f2ab3..b8e9c78479 100644 --- a/dsl/description.go +++ b/dsl/description.go @@ -45,6 +45,8 @@ func Description(d string) { e.Description = d case *expr.GRPCResponseExpr: e.Description = d + case *expr.InterceptorExpr: + e.Description = d default: eval.IncompatibleDSL() } diff --git a/dsl/interceptor.go b/dsl/interceptor.go new file mode 100644 index 0000000000..28b99ce7dd --- /dev/null +++ b/dsl/interceptor.go @@ -0,0 +1,293 @@ +package dsl + +import ( + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +// Interceptor defines a request interceptor. Interceptors provide a type-safe way +// to read and write from and to the request and response. +// +// Interceptor must appear in a API, Service or Method expression. +// +// Interceptor accepts two arguments: the name of the interceptor and the +// defining DSL. +// +// Example: +// +// var Cache = Interceptor("Cache", func() { +// Description("Server-side interceptor which implements a transparent cache for the loaded records") +// +// ReadPayload(func() { +// Attribute("id") +// }) +// +// WriteResult(func() { +// Attribute("cachedAt") +// }) +// }) +func Interceptor(name string, fn ...func()) *expr.InterceptorExpr { + if len(fn) > 1 { + eval.ReportError("interceptor %q cannot have multiple definitions", name) + return nil + } + i := &expr.InterceptorExpr{Name: name} + if name == "" { + eval.ReportError("interceptor name cannot be empty") + return i + } + if len(fn) > 0 { + if !eval.Execute(fn[0], i) { + return i + } + } + for _, i := range expr.Root.Interceptors { + if i.Name == name { + eval.ReportError("interceptor %q already defined", name) + return i + } + } + expr.Root.Interceptors = append(expr.Root.Interceptors, i) + return i +} + +// ReadPayload defines the payload attributes read by the interceptor. +// +// ReadPayload must appear in an interceptor DSL. +// +// ReadPayload takes a function as argument which can use the Attribute DSL to +// define the attributes read by the interceptor. +// +// Example: +// +// ReadPayload(func() { +// Attribute("id") +// }) +// +// ReadPayload also accepts user defined types: +// +// // Interceptor can read any payload field +// ReadPayload(MethodPayload) +func ReadPayload(arg any) { + setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) { + i.ReadPayload = attr + }) +} + +// WritePayload defines the payload attributes written by the interceptor. +// +// WritePayload must appear in an interceptor DSL. +// +// WritePayload takes a function as argument which can use the Attribute DSL to +// define the attributes written by the interceptor. +// +// Example: +// +// WritePayload(func() { +// Attribute("auth") +// }) +// +// WritePayload also accepts user defined types: +// +// // Interceptor can write any payload field +// WritePayload(MethodPayload) +func WritePayload(arg any) { + setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) { + i.WritePayload = attr + }) +} + +// ReadResult defines the result attributes read by the interceptor. +// +// ReadResult must appear in an interceptor DSL. +// +// ReadResult takes a function as argument which can use the Attribute DSL to +// define the attributes read by the interceptor. +// +// Example: +// +// ReadResult(func() { +// Attribute("cachedAt") +// }) +// +// ReadResult also accepts user defined types: +// +// // Interceptor can read any result field +// ReadResult(MethodResult) +func ReadResult(arg any) { + setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) { + i.ReadResult = attr + }) +} + +// WriteResult defines the result attributes written by the interceptor. +// +// WriteResult must appear in an interceptor DSL. +// +// WriteResult takes a function as argument which can use the Attribute DSL to +// define the attributes written by the interceptor. +// +// Example: +// +// WriteResult(func() { +// Attribute("cachedAt") +// }) +// +// WriteResult also accepts user defined types: +// +// // Interceptor can write any result field +// WriteResult(MethodResult) +func WriteResult(arg any) { + setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) { + i.WriteResult = attr + }) +} + +// ServerInterceptor lists the server-side interceptors that apply to all the +// API endpoints, all the service endpoints or a specific endpoint. +// +// ServerInterceptor must appear in a API, Service or Method expression. +// +// ServerInterceptor accepts one or more interceptor or interceptor names as +// arguments. ServerInterceptor can appear multiple times in the same DSL. +// +// Example: +// +// Method("get_record", func() { +// // Interceptor defined with the Interceptor DSL +// ServerInterceptor(SetDeadline) +// +// // Name of interceptor defined with the Interceptor DSL +// ServerInterceptor("Cache") +// +// // Interceptor defined inline +// ServerInterceptor(Interceptor("CheckUserID", func() { +// ReadPayload(func() { +// Attribute("auth") +// }) +// })) +// +// // ... rest of the method DSL +// }) +func ServerInterceptor(interceptors ...any) { + addInterceptors(interceptors, false) +} + +// ClientInterceptor lists the client-side interceptors that apply to all the +// API endpoints, all the service endpoints or a specific endpoint. +// +// ClientInterceptor must appear in a API, Service or Method expression. +// +// ClientInterceptor accepts one or more interceptor or interceptor names as +// arguments. ClientInterceptor can appear multiple times in the same DSL. +// +// Example: +// +// Method("get_record", func() { +// // Interceptor defined with the Interceptor DSL +// ClientInterceptor(Retry) +// +// // Name of interceptor defined with the Interceptor DSL +// ClientInterceptor("Cache") +// +// // Interceptor defined inline +// ClientInterceptor(Interceptor("Sign", func() { +// ReadPayload(func() { +// Attribute("user_id") +// }) +// WritePayload(func() { +// Attribute("auth") +// }) +// })) +// +// // ... rest of the method DSL +// }) +func ClientInterceptor(interceptors ...any) { + addInterceptors(interceptors, true) +} + +// setInterceptorAttribute is a helper function that handles the common logic for +// setting interceptor attributes (ReadPayload, WritePayload, ReadResult, WriteResult). +func setInterceptorAttribute(arg any, setter func(i *expr.InterceptorExpr, attr *expr.AttributeExpr)) { + i, ok := eval.Current().(*expr.InterceptorExpr) + if !ok { + eval.IncompatibleDSL() + return + } + + var attr *expr.AttributeExpr + switch fn := arg.(type) { + case func(): + attr = &expr.AttributeExpr{Type: &expr.Object{}} + if !eval.Execute(fn, attr) { + return + } + case *expr.AttributeExpr: + attr = fn + case expr.DataType: + attr = &expr.AttributeExpr{Type: fn} + default: + eval.InvalidArgError("type, attribute or func()", arg) + return + } + setter(i, attr) +} + +// addInterceptors is a helper function that validates and adds interceptors to +// the current expression. +func addInterceptors(interceptors []any, client bool) { + kind := "ServerInterceptor" + if client { + kind = "ClientInterceptor" + } + if len(interceptors) == 0 { + eval.ReportError("%s: at least one interceptor must be specified", kind) + return + } + + var ints []*expr.InterceptorExpr + for _, i := range interceptors { + switch i := i.(type) { + case *expr.InterceptorExpr: + ints = append(ints, i) + case string: + var found bool + for _, in := range expr.Root.Interceptors { + if in.Name == i { + ints = append(ints, in) + found = true + break + } + } + if !found { + eval.ReportError("%s: interceptor %q not found", kind, i) + } + default: + eval.ReportError("%s: invalid interceptor %v", kind, i) + } + } + + current := eval.Current() + switch actual := current.(type) { + case *expr.APIExpr: + if client { + actual.ClientInterceptors = append(actual.ClientInterceptors, ints...) + } else { + actual.ServerInterceptors = append(actual.ServerInterceptors, ints...) + } + case *expr.ServiceExpr: + if client { + actual.ClientInterceptors = append(actual.ClientInterceptors, ints...) + } else { + actual.ServerInterceptors = append(actual.ServerInterceptors, ints...) + } + case *expr.MethodExpr: + if client { + actual.ClientInterceptors = append(actual.ClientInterceptors, ints...) + } else { + actual.ServerInterceptors = append(actual.ServerInterceptors, ints...) + } + default: + eval.IncompatibleDSL() + } +} diff --git a/dsl/interceptor_test.go b/dsl/interceptor_test.go new file mode 100644 index 0000000000..7e519768d1 --- /dev/null +++ b/dsl/interceptor_test.go @@ -0,0 +1,250 @@ +package dsl_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "goa.design/goa/v3/dsl" + "goa.design/goa/v3/eval" + "goa.design/goa/v3/expr" +) + +func TestInterceptor(t *testing.T) { + cases := map[string]struct { + DSL func() + Assert func(t *testing.T, intr *expr.InterceptorExpr) + }{ + "valid-minimal": { + func() { + Interceptor("minimal", func() {}) + }, + func(t *testing.T, intr *expr.InterceptorExpr) { + require.NotNil(t, intr, "interceptor should not be nil") + assert.Equal(t, "minimal", intr.Name) + }, + }, + "valid-complete": { + func() { + Interceptor("complete", func() { + Description("test interceptor") + ReadPayload(func() { + Attribute("foo", String) + }) + WritePayload(func() { + Attribute("bar", String) + }) + ReadResult(func() { + Attribute("baz", String) + }) + WriteResult(func() { + Attribute("qux", String) + }) + }) + }, + func(t *testing.T, intr *expr.InterceptorExpr) { + require.NotNil(t, intr, "interceptor should not be nil") + assert.Equal(t, "test interceptor", intr.Description) + + require.NotNil(t, intr.ReadPayload, "ReadPayload should not be nil") + rp := expr.AsObject(intr.ReadPayload.Type) + require.NotNil(t, rp, "ReadPayload should be an object") + assert.NotNil(t, rp.Attribute("foo"), "ReadPayload should have a foo attribute") + + require.NotNil(t, intr.WritePayload, "WritePayload should not be nil") + wp := expr.AsObject(intr.WritePayload.Type) + require.NotNil(t, wp, "WritePayload should be an object") + assert.NotNil(t, wp.Attribute("bar"), "WritePayload should have a bar attribute") + + require.NotNil(t, intr.ReadResult, "ReadResult should not be nil") + rr := expr.AsObject(intr.ReadResult.Type) + require.NotNil(t, rr, "ReadResult should be an object") + assert.NotNil(t, rr.Attribute("baz"), "ReadResult should have a baz attribute") + + require.NotNil(t, intr.WriteResult, "WriteResult should not be nil") + wr := expr.AsObject(intr.WriteResult.Type) + require.NotNil(t, wr, "WriteResult should be an object") + assert.NotNil(t, wr.Attribute("qux"), "WriteResult should have a qux attribute") + }, + }, + "empty-name": { + func() { + Interceptor("", func() {}) + }, + func(t *testing.T, intr *expr.InterceptorExpr) { + assert.NotNil(t, eval.Context.Errors, "expected a validation error") + }, + }, + "duplicate-name": { + func() { + Interceptor("duplicate", func() {}) + Interceptor("duplicate", func() {}) + }, + func(t *testing.T, intr *expr.InterceptorExpr) { + if eval.Context.Errors == nil { + t.Error("expected a validation error, got none") + } + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eval.Context = &eval.DSLContext{} + expr.Root = new(expr.RootExpr) + tc.DSL() + if len(expr.Root.Interceptors) > 0 { + tc.Assert(t, expr.Root.Interceptors[0]) + } + }) + } +} + +func TestServerInterceptor(t *testing.T) { + cases := map[string]struct { + DSL func() + Assert func(t *testing.T, svc *expr.ServiceExpr, err error) + }{ + "valid-reference": { + func() { + var testInterceptor = Interceptor("test", func() {}) + Service("Service", func() { + ServerInterceptor(testInterceptor) + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.NoError(t, err) + require.NotNil(t, svc) + require.Len(t, svc.ServerInterceptors, 1, "should have 1 server interceptor") + assert.Equal(t, "test", svc.ServerInterceptors[0].Name) + }, + }, + "valid-by-name": { + func() { + Interceptor("test", func() {}) + Service("Service", func() { + ServerInterceptor("test") + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.NoError(t, err) + require.NotNil(t, svc) + require.Len(t, svc.ServerInterceptors, 1, "should have 1 server interceptor") + assert.Equal(t, "test", svc.ServerInterceptors[0].Name) + }, + }, + "invalid-reference": { + func() { + Service("Service", func() { + ServerInterceptor(42) // Invalid type + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.Error(t, err) + }, + }, + "invalid-name": { + func() { + Service("Service", func() { + ServerInterceptor("invalid") + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.Error(t, err) + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eval.Context = &eval.DSLContext{} + expr.Root = new(expr.RootExpr) + tc.DSL() + root, err := runDSL(t, tc.DSL) + tc.Assert(t, root.Services[0], err) + }) + } +} + +func TestClientInterceptor(t *testing.T) { + cases := map[string]struct { + DSL func() + Assert func(t *testing.T, svc *expr.ServiceExpr, err error) + }{ + "valid-reference": { + func() { + var testInterceptor = Interceptor("test", func() {}) + Service("Service", func() { + ClientInterceptor(testInterceptor) + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.NoError(t, err) + require.NotNil(t, svc) + require.Len(t, svc.ClientInterceptors, 1, "should have 1 client interceptor") + }, + }, + "valid-by-name": { + func() { + Interceptor("test", func() {}) + Service("Service", func() { + ClientInterceptor("test") + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.NoError(t, err) + require.NotNil(t, svc) + require.Len(t, svc.ClientInterceptors, 1, "should have 1 client interceptor") + }, + }, + "invalid-reference": { + func() { + Service("Service", func() { + ClientInterceptor(42) // Invalid type + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.Error(t, err) + }, + }, + "invalid-name": { + func() { + Service("Service", func() { + ClientInterceptor("invalid") + }) + }, + func(t *testing.T, svc *expr.ServiceExpr, err error) { + require.Error(t, err) + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + eval.Context = &eval.DSLContext{} + expr.Root = new(expr.RootExpr) + tc.DSL() + root, err := runDSL(t, tc.DSL) + tc.Assert(t, root.Services[0], err) + }) + } +} + +// runDSL returns the DSL root resulting from running the given DSL. +func runDSL(t *testing.T, dsl func()) (*expr.RootExpr, error) { + t.Helper() + eval.Reset() + expr.Root = new(expr.RootExpr) + expr.GeneratedResultTypes = new(expr.ResultTypesRoot) + require.NoError(t, eval.Register(expr.Root)) + require.NoError(t, eval.Register(expr.GeneratedResultTypes)) + expr.Root.API = expr.NewAPIExpr("test api", func() {}) + expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()} + if eval.Execute(dsl, nil) { + return expr.Root, eval.RunDSL() + } else { + return expr.Root, errors.New(eval.Context.Error()) + } +} diff --git a/dsl/meta.go b/dsl/meta.go index 05f917f851..60ffb196f4 100644 --- a/dsl/meta.go +++ b/dsl/meta.go @@ -288,3 +288,42 @@ func Meta(name string, value ...string) { eval.IncompatibleDSL() } } + +// RemoveMeta removes a meta key from an object. +// +// RemoveMeta may appear where Meta can appear. +// +// RemoveMeta takes a single argument, the name of the meta key to remove. +func RemoveMeta(name string) { + switch e := eval.Current().(type) { + case *expr.APIExpr: + delete(e.Meta, name) + case *expr.ServerExpr: + delete(e.Meta, name) + case *expr.HostExpr: + delete(e.Meta, name) + case *expr.AttributeExpr: + delete(e.Meta, name) + case *expr.ResultTypeExpr: + delete(e.Meta, name) + case *expr.MethodExpr: + delete(e.Meta, name) + case *expr.ServiceExpr: + delete(e.Meta, name) + case *expr.HTTPServiceExpr: + delete(e.Meta, name) + case *expr.HTTPEndpointExpr: + delete(e.Meta, name) + case *expr.RouteExpr: + delete(e.Meta, name) + case *expr.HTTPFileServerExpr: + delete(e.Meta, name) + case *expr.HTTPResponseExpr: + delete(e.Meta, name) + case expr.CompositeExpr: + att := e.Attribute() + delete(att.Meta, name) + default: + eval.IncompatibleDSL() + } +} diff --git a/expr/api.go b/expr/api.go index 01ed202620..7d77fb975e 100644 --- a/expr/api.go +++ b/expr/api.go @@ -36,6 +36,10 @@ type ( // potentially multiple schemes. Incoming requests must validate // at least one requirement to be authorized. Requirements []*SecurityExpr + // ClientInterceptors is the list of API client interceptors. + ClientInterceptors []*InterceptorExpr + // ServerInterceptors is the list of API server interceptors. + ServerInterceptors []*InterceptorExpr // HTTP contains the HTTP specific API level expressions. HTTP *HTTPExpr // GRPC contains the gRPC specific API level expressions. diff --git a/expr/interceptor.go b/expr/interceptor.go new file mode 100644 index 0000000000..538d85463f --- /dev/null +++ b/expr/interceptor.go @@ -0,0 +1,84 @@ +package expr + +import ( + "goa.design/goa/v3/eval" +) + +type ( + // InterceptorExpr describes an interceptor definition in the design. + // Interceptors are used to inject user code into the request/response processing pipeline. + // There are four kinds of interceptors, in order of execution: + // * client-side payload: executes after the payload is encoded and before the request is sent to the server + // * server-side request: executes after the request is decoded and before the payload is sent to the service + // * server-side result: executes after the service returns a result and before the response is encoded + // * client-side response: executes after the response is decoded and before the result is sent to the client + InterceptorExpr struct { + // Name is the name of the interceptor + Name string + // Description is the optional description of the interceptor + Description string + // ReadPayload lists the payload attribute names read by the interceptor + ReadPayload *AttributeExpr + // WritePayload lists the payload attribute names written by the interceptor + WritePayload *AttributeExpr + // ReadResult lists the result attribute names read by the interceptor + ReadResult *AttributeExpr + // WriteResult lists the result attribute names written by the interceptor + WriteResult *AttributeExpr + } +) + +// EvalName returns the generic expression name used in error messages. +func (i *InterceptorExpr) EvalName() string { + return "interceptor " + i.Name +} + +// validate validates the interceptor. +func (i *InterceptorExpr) validate(m *MethodExpr) *eval.ValidationErrors { + verr := new(eval.ValidationErrors) + + if i.ReadPayload != nil || i.WritePayload != nil { + payloadObj := AsObject(m.Payload.Type) + if payloadObj == nil { + verr.Add(m, "interceptor %q cannot be applied because the method payload is not an object", i.Name) + } + if i.ReadPayload != nil { + i.validateAttributeAccess(m, "read payload", verr, payloadObj, i.ReadPayload) + } + if i.WritePayload != nil { + i.validateAttributeAccess(m, "write payload", verr, payloadObj, i.WritePayload) + } + } + + if i.ReadResult != nil || i.WriteResult != nil { + if m.IsResultStreaming() { + verr.Add(m, "interceptor %q cannot be applied because the method result is streaming", i.Name) + } + resultObj := AsObject(m.Result.Type) + if resultObj == nil { + verr.Add(m, "interceptor %q cannot be applied because the method result is not an object", i.Name) + } + if i.ReadResult != nil { + i.validateAttributeAccess(m, "read result", verr, resultObj, i.ReadResult) + } + if i.WriteResult != nil { + i.validateAttributeAccess(m, "write result", verr, resultObj, i.WriteResult) + } + } + + return verr +} + +// validateAttributeAccess validates that all attributes in attr exist in obj +func (i *InterceptorExpr) validateAttributeAccess(m *MethodExpr, source string, verr *eval.ValidationErrors, obj *Object, attr *AttributeExpr) { + attrObj := AsObject(attr.Type) + if attrObj == nil { + verr.Add(m, "interceptor %q %s attribute is not an object", i.Name, source) + return + } + for _, att := range *attrObj { + if obj.Attribute(att.Name) == nil { + verr.Add(m, "interceptor %q cannot %s attribute %q: attribute does not exist", i.Name, source, att.Name) + } + } +} diff --git a/expr/method.go b/expr/method.go index 3ba80728bd..94e3e59356 100644 --- a/expr/method.go +++ b/expr/method.go @@ -32,6 +32,10 @@ type ( // schemes. Incoming requests must validate at least one // requirement to be authorized. Requirements []*SecurityExpr + // ClientInterceptors is the list of client interceptors. + ClientInterceptors []*InterceptorExpr + // ServerInterceptors is the list of server interceptors. + ServerInterceptors []*InterceptorExpr // Service that owns method. Service *ServiceExpr // Meta is an arbitrary set of key/value pairs, see dsl.Meta @@ -84,7 +88,8 @@ func (m *MethodExpr) EvalName() string { } // Prepare makes sure the payload and result types are initialized (to the Empty -// type if nil). +// type if nil) and merges the method interceptors with the API and service level +// interceptors. func (m *MethodExpr) Prepare() { if m.Payload == nil { m.Payload = &AttributeExpr{Type: Empty} @@ -95,13 +100,57 @@ func (m *MethodExpr) Prepare() { if m.Result == nil { m.Result = &AttributeExpr{Type: Empty} } + + m.ClientInterceptors = mergeInterceptors(m.ClientInterceptors, m.Service.ClientInterceptors, Root.API.ClientInterceptors) + m.ServerInterceptors = mergeInterceptors(m.ServerInterceptors, m.Service.ServerInterceptors, Root.API.ServerInterceptors) +} + +// mergeInterceptors merges interceptors from different levels (method, service, API) +// while avoiding duplicates. The order of precedence is: method > service > API. +func mergeInterceptors(methodLevel, serviceLevel, apiLevel []*InterceptorExpr) []*InterceptorExpr { + existing := make(map[string]struct{}) + result := make([]*InterceptorExpr, 0, len(methodLevel)+len(serviceLevel)+len(apiLevel)) + + // Add method-level interceptors + for _, i := range methodLevel { + existing[i.Name] = struct{}{} + result = append(result, i) + } + + // Add service-level interceptors + for _, i := range serviceLevel { + if _, ok := existing[i.Name]; !ok { + result = append(result, i) + existing[i.Name] = struct{}{} + } + } + + // Add API-level interceptors + for _, i := range apiLevel { + if _, ok := existing[i.Name]; !ok { + result = append(result, i) + } + } + + return result } -// Validate validates the method payloads, results, and errors (if any). +// Validate validates the method payloads, results, errors, security +// requirements, and interceptors. func (m *MethodExpr) Validate() error { verr := new(eval.ValidationErrors) verr.Merge(m.Payload.Validate("payload", m)) - // validate security scheme requirements + verr.Merge(m.StreamingPayload.Validate("streaming_payload", m)) + verr.Merge(m.Result.Validate("result", m)) + verr.Merge(m.validateRequirements()) + verr.Merge(m.validateErrors()) + verr.Merge(m.validateInterceptors()) + return verr +} + +// validateRequirements validates the security requirements. +func (m *MethodExpr) validateRequirements() *eval.ValidationErrors { + verr := new(eval.ValidationErrors) var requirements []*SecurityExpr if len(m.Requirements) > 0 { requirements = m.Requirements @@ -185,12 +234,12 @@ func (m *MethodExpr) Validate() error { verr.Add(m, "payload of method %q of service %q defines a OAuth2 access token attribute, but no OAuth2 security scheme exist", m.Name, m.Service.Name) } } - if m.StreamingPayload.Type != Empty { - verr.Merge(m.StreamingPayload.Validate("streaming_payload", m)) - } - if m.Result.Type != Empty { - verr.Merge(m.Result.Validate("result", m)) - } + return verr +} + +// validateErrors validates the method errors. +func (m *MethodExpr) validateErrors() *eval.ValidationErrors { + verr := new(eval.ValidationErrors) for i, e := range m.Errors { if err := e.Validate(); err != nil { var verrs *eval.ValidationErrors @@ -220,6 +269,18 @@ func (m *MethodExpr) Validate() error { return verr } +// validateInterceptors validates the method interceptors. +func (m *MethodExpr) validateInterceptors() *eval.ValidationErrors { + verr := new(eval.ValidationErrors) + for _, i := range m.ClientInterceptors { + verr.Merge(i.validate(m)) + } + for _, i := range m.ServerInterceptors { + verr.Merge(i.validate(m)) + } + return verr +} + // hasTag is a helper function that traverses the given attribute and all its // bases recursively looking for an attribute with the given tag meta. This // recursion is only needed for attributes that have not been finalized yet. diff --git a/expr/root.go b/expr/root.go index 0240e54e1f..fb215c40f9 100644 --- a/expr/root.go +++ b/expr/root.go @@ -18,6 +18,8 @@ type ( API *APIExpr // Services contains the list of services exposed by the API. Services []*ServiceExpr + // Interceptors contains the list of interceptors. + Interceptors []*InterceptorExpr // Errors contains the list of errors returned by all the API // methods. Errors []*ErrorExpr diff --git a/expr/service.go b/expr/service.go index 1a7d88e9cf..ab906c4688 100644 --- a/expr/service.go +++ b/expr/service.go @@ -27,6 +27,10 @@ type ( // potentially multiple schemes. Incoming requests must validate // at least one requirement to be authorized. Requirements []*SecurityExpr + // ClientInterceptors is the list of client interceptors. + ClientInterceptors []*InterceptorExpr + // ServerInterceptors is the list of server interceptors. + ServerInterceptors []*InterceptorExpr // Meta is a set of key/value pairs with semantic that is // specific to each generator. Meta MetaExpr diff --git a/go.mod b/go.mod index 17d9e52fcc..f2a5d754de 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module goa.design/goa/v3 -go 1.22.0 +go 1.22.7 -toolchain go1.23.1 +toolchain go1.23.3 require ( github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 @@ -14,9 +14,9 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.10.0 golang.org/x/text v0.21.0 - golang.org/x/tools v0.28.0 - google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.36.1 + golang.org/x/tools v0.29.0 + google.golang.org/grpc v1.69.4 + google.golang.org/protobuf v1.36.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -32,8 +32,8 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + golang.org/x/sys v0.29.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect ) diff --git a/go.sum b/go.sum index 7af0c27c31..2c8593e441 100644 --- a/go.sum +++ b/go.sum @@ -64,22 +64,22 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go index c1818960c9..b570c4828b 100644 --- a/http/codegen/service_data.go +++ b/http/codegen/service_data.go @@ -595,7 +595,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { scope := codegen.NewNameScope() scope.Unique("c") // 'c' is reserved as the client's receiver name. scope.Unique("v") // 'v' is reserved as the request builder payload argument name. - rd := &ServiceData{ + sd := &ServiceData{ Service: svc, ServerStruct: "Server", MountPointStruct: "MountPoint", @@ -641,7 +641,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { VarName: scope.Unique(codegen.Goify(s.FilePath, true)), ArgName: scope.Unique(fmt.Sprintf("fileSystem%s", codegen.Goify(s.FilePath, true))), } - rd.FileServers = append(rd.FileServers, data) + sd.FileServers = append(sd.FileServers, data) } for _, httpEndpoint := range httpSvc.HTTPEndpoints { @@ -672,10 +672,10 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { // Path params may override requiredness, need to check payload. pointer = httpEndpoint.MethodExpr.Payload.IsPrimitivePointer(arg, true) } - name := rd.Scope.Name(codegen.Goify(arg, false)) + name := sd.Scope.Name(codegen.Goify(arg, false)) var vcode string if att.Validation != nil { - ctx := httpContext("", rd.Scope, true, false) + ctx := httpContext("", sd.Scope, true, false) vcode = codegen.AttributeValidationCode(att, nil, ctx, true, expr.IsAlias(att.Type), name, arg) } initArgs[j] = &InitArgData{ @@ -686,8 +686,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { Description: att.Description, FieldName: codegen.Goify(arg, true), FieldType: patt.Type, - TypeName: rd.Scope.GoTypeName(att), - TypeRef: rd.Scope.GoTypeRef(att), + TypeName: sd.Scope.GoTypeName(att), + TypeRef: sd.Scope.GoTypeRef(att), Type: att.Type, Pointer: pointer, Required: true, @@ -727,7 +727,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { } } - payload := buildPayloadData(httpEndpoint, rd) + payload := buildPayloadData(httpEndpoint, sd) var ( reqs service.RequirementsData @@ -817,8 +817,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { ServiceVarName: svc.VarName, ServicePkgName: svc.PkgName, Payload: payload, - Result: buildResultData(httpEndpoint, rd), - Errors: buildErrorsData(httpEndpoint, rd), + Result: buildResultData(httpEndpoint, sd), + Errors: buildErrorsData(httpEndpoint, sd), HeaderSchemes: hsch, BodySchemes: bosch, QuerySchemes: qsch, @@ -837,7 +837,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { Requirements: reqs, } if httpEndpoint.MethodExpr.IsStreaming() { - initWebSocketData(ed, httpEndpoint, rd) + initWebSocketData(ed, httpEndpoint, sd) } if httpEndpoint.MultipartRequest { @@ -870,26 +870,26 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { } } - rd.Endpoints = append(rd.Endpoints, ed) + sd.Endpoints = append(sd.Endpoints, ed) } for _, a := range httpSvc.HTTPEndpoints { collectUserTypes(a.Body.Type, func(ut expr.UserType) { - if d := attributeTypeData(ut, true, true, true, rd); d != nil { - rd.ServerBodyAttributeTypes = append(rd.ServerBodyAttributeTypes, d) + if d := attributeTypeData(ut, true, true, true, sd); d != nil { + sd.ServerBodyAttributeTypes = append(sd.ServerBodyAttributeTypes, d) } - if d := attributeTypeData(ut, true, false, false, rd); d != nil { - rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d) + if d := attributeTypeData(ut, true, false, false, sd); d != nil { + sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d) } }) if a.MethodExpr.StreamingPayload.Type != expr.Empty { collectUserTypes(a.StreamingBody.Type, func(ut expr.UserType) { - if d := attributeTypeData(ut, true, true, true, rd); d != nil { - rd.ServerBodyAttributeTypes = append(rd.ServerBodyAttributeTypes, d) + if d := attributeTypeData(ut, true, true, true, sd); d != nil { + sd.ServerBodyAttributeTypes = append(sd.ServerBodyAttributeTypes, d) } - if d := attributeTypeData(ut, true, false, false, rd); d != nil { - rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d) + if d := attributeTypeData(ut, true, false, false, sd); d != nil { + sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d) } }) } @@ -900,8 +900,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { // NOTE: ServerBodyAttributeTypes for response body types are // collected in buildResponseBodyType because we have to generate // body types for each view in a result type. - if d := attributeTypeData(ut, false, true, false, rd); d != nil { - rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d) + if d := attributeTypeData(ut, false, true, false, sd); d != nil { + sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d) } }) } @@ -912,14 +912,14 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData { // NOTE: ServerBodyAttributeTypes for error response body types are // collected in buildResponseBodyType because we have to generate // body types for each view in a result type. - if d := attributeTypeData(ut, false, true, false, rd); d != nil { - rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d) + if d := attributeTypeData(ut, false, true, false, sd); d != nil { + sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d) } }) } } - return rd + return sd } // makeHTTPType traverses the attribute recursively and performs these actions: diff --git a/pkg/interceptor.go b/pkg/interceptor.go new file mode 100644 index 0000000000..022d7a4de6 --- /dev/null +++ b/pkg/interceptor.go @@ -0,0 +1,22 @@ +package goa + +import "context" + +type ( + // InterceptorInfo contains information about the request shared between + // all interceptors in the service chain. It provides access to the service name, + // method name, endpoint function, and request payload. + InterceptorInfo struct { + // Name of service handling request + Service string + // Name of method handling request + Method string + // Endpoint of request, can be used for retrying + Endpoint Endpoint + // Payload of request + RawPayload any + } + + // NextFunc is a function that will continue the request processing chain. + NextFunc func(ctx context.Context) (any, error) +) From fde6f80f7ce1131e643824ced145771d839dba0d Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Tue, 26 Nov 2024 13:14:30 -0800 Subject: [PATCH 2/6] Remove Twitter badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index cd38d1919e..7735bc20e3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Slack: Goa Slack: Sign-up BSky: Goa - Twitter: @goadesign

From c2caae636ae90d3a58f29370fdbc80977e716ad7 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Tue, 26 Nov 2024 13:30:40 -0800 Subject: [PATCH 3/6] Improve generated comments --- codegen/service/interceptors.go | 2 ++ codegen/service/templates/client_wrappers.go.tpl | 2 +- codegen/service/templates/endpoint_wrappers.go.tpl | 2 +- codegen/service/testdata/client_code.go | 2 +- codegen/service/testdata/endpoint_code.go | 8 ++++---- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/codegen/service/interceptors.go b/codegen/service/interceptors.go index 57d734e0c6..a383ddbb24 100644 --- a/codegen/service/interceptors.go +++ b/codegen/service/interceptors.go @@ -33,6 +33,7 @@ type ( // InterceptorData describes a single interceptor. InterceptorData struct { Name string + DesignName string UnexportedName string Description string PayloadRef string @@ -150,6 +151,7 @@ func buildMethodInterceptors(m *expr.MethodExpr, scope *codegen.NameScope) ([]*I return &InterceptorData{ Name: codegen.Goify(intr.Name, true), + DesignName: intr.Name, UnexportedName: codegen.Goify(intr.Name, false), Description: intr.Description, PayloadRef: methodData.PayloadRef, diff --git a/codegen/service/templates/client_wrappers.go.tpl b/codegen/service/templates/client_wrappers.go.tpl index 94364c38cd..dd52071dd9 100644 --- a/codegen/service/templates/client_wrappers.go.tpl +++ b/codegen/service/templates/client_wrappers.go.tpl @@ -7,7 +7,7 @@ func Wrap{{ .MethodVarName }}ClientEndpoint(endpoint goa.Endpoint, i ClientInter } {{- range .ClientInterceptors }} -{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .Name) }} +{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .DesignName) }} func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &{{ .Name }}Info{ diff --git a/codegen/service/templates/endpoint_wrappers.go.tpl b/codegen/service/templates/endpoint_wrappers.go.tpl index bae0b9fa25..63dcf39e1e 100644 --- a/codegen/service/templates/endpoint_wrappers.go.tpl +++ b/codegen/service/templates/endpoint_wrappers.go.tpl @@ -7,7 +7,7 @@ func Wrap{{ .MethodVarName }}Endpoint(endpoint goa.Endpoint, i ServerInterceptor } {{- range .ServerInterceptors }} -{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .Name) }} +{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .DesignName) }} func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &{{ .Name }}Info{ diff --git a/codegen/service/testdata/client_code.go b/codegen/service/testdata/client_code.go index 96f5a7b6a0..8153742480 100644 --- a/codegen/service/testdata/client_code.go +++ b/codegen/service/testdata/client_code.go @@ -315,7 +315,7 @@ func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.E return endpoint } -// wrapClientTracing applies the Tracing interceptor to endpoints. +// wrapClientTracing applies the tracing interceptor to endpoints. func wrapClientTracing(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &TracingInfo{ diff --git a/codegen/service/testdata/endpoint_code.go b/codegen/service/testdata/endpoint_code.go index 27967a6e08..98db14f0d1 100644 --- a/codegen/service/testdata/endpoint_code.go +++ b/codegen/service/testdata/endpoint_code.go @@ -551,7 +551,7 @@ func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoin return endpoint } -// wrapLogging applies the Logging interceptor to endpoints. +// wrapLogging applies the logging interceptor to endpoints. func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ @@ -605,7 +605,7 @@ func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoin return endpoint } -// wrapLogging applies the Logging interceptor to endpoints. +// wrapLogging applies the logging interceptor to endpoints. func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ @@ -621,7 +621,7 @@ func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa } } -// wrapMetrics applies the Metrics interceptor to endpoints. +// wrapMetrics applies the metrics interceptor to endpoints. func wrapMetrics(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &MetricsInfo{ @@ -679,7 +679,7 @@ func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoin return endpoint } -// wrapLogging applies the Logging interceptor to endpoints. +// wrapLogging applies the logging interceptor to endpoints. func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ From 6d9bc66cf3112c928bc4cff625ec26b8e7c6b8ed Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Thu, 19 Dec 2024 10:50:11 -0400 Subject: [PATCH 4/6] Add support for interceptors in generated examples --- codegen/cli/cli.go | 28 +- codegen/example/example_server.go | 20 +- .../example/templates/server_endpoints.go.tpl | 2 +- .../templates/server_interceptors.go.tpl | 23 ++ codegen/generator/example.go | 7 + codegen/service/client.go | 12 - codegen/service/endpoint.go | 46 +-- codegen/service/example_interceptors.go | 86 ++++++ codegen/service/example_interceptors_test.go | 119 ++++++++ codegen/service/example_svc.go | 6 +- codegen/service/interceptors.go | 280 +++++++----------- codegen/service/interceptors_test.go | 63 ++-- codegen/service/service.go | 8 +- codegen/service/service_data.go | 228 +++++++++++--- .../client_interceptor_wrappers.go.tpl | 18 ++ .../templates/client_interceptors.go.tpl | 12 + .../service/templates/client_wrappers.go.tpl | 22 -- .../templates/endpoint_wrappers.go.tpl | 23 -- .../example_client_interceptor.go.tpl | 24 ++ ....tpl => example_security_authfuncs.go.tpl} | 0 .../example_server_interceptor.go.tpl | 24 ++ ...nit.go.tpl => example_service_init.go.tpl} | 0 ...t.go.tpl => example_service_struct.go.tpl} | 0 codegen/service/templates/interceptors.go.tpl | 35 +-- .../server_interceptor_wrappers.go.tpl | 18 ++ .../templates/server_interceptors.go.tpl | 12 + codegen/service/testdata/client_code.go | 24 +- codegen/service/testdata/endpoint_code.go | 89 +----- .../api_interceptor_service_client.golden | 35 +++ .../api_interceptor_service_server.golden | 35 +++ .../chained_interceptor_service_client.golden | 55 ++++ .../chained_interceptor_service_server.golden | 55 ++++ .../client_interceptor_service_client.golden | 35 +++ ...ultiple_interceptors_service_client.golden | 45 +++ ...ultiple_interceptors_service_server.golden | 45 +++ ...rvices_interceptors_service2_client.golden | 45 +++ ...rvices_interceptors_service2_server.golden | 45 +++ ...ervices_interceptors_service_client.golden | 45 +++ ...ervices_interceptors_service_server.golden | 45 +++ ..._interceptor_by_name_service_server.golden | 35 +++ .../server_interceptor_service_server.golden | 35 +++ .../testdata/example_interceptors_dsls.go | 96 ++++++ ...read-payload_client_interceptors.go.golden | 14 + ...ead-payload_interceptor_wrappers.go.golden | 26 ++ ...ad-payload_service_interceptors.go.golden} | 15 +- ...-read-result_client_interceptors.go.golden | 14 + ...read-result_interceptor_wrappers.go.golden | 26 ++ ...ead-result_service_interceptors.go.golden} | 15 +- ...rite-payload_client_interceptors.go.golden | 14 + ...ite-payload_interceptor_wrappers.go.golden | 26 ++ ...te-payload_service_interceptors.go.golden} | 15 +- ...write-result_client_interceptors.go.golden | 14 + ...rite-result_interceptor_wrappers.go.golden | 26 ++ ...ite-result_service_interceptors.go.golden} | 15 +- ...rite-payload_client_interceptors.go.golden | 14 + ...ite-payload_interceptor_wrappers.go.golden | 26 ++ ...te-payload_service_interceptors.go.golden} | 15 +- ...write-result_client_interceptors.go.golden | 14 + ...rite-result_interceptor_wrappers.go.golden | 26 ++ ...ite-result_service_interceptors.go.golden} | 15 +- .../interceptors/multiple-interceptors.golden | 30 -- ...interceptors_client_interceptors.go.golden | 26 ++ ...nterceptors_interceptor_wrappers.go.golden | 52 ++++ ...nterceptors_service_interceptors.go.golden | 26 ++ .../single-api-server-interceptor.golden | 16 - ...interceptor_interceptor_wrappers.go.golden | 13 + ...interceptor_service_interceptors.go.golden | 28 ++ .../single-client-interceptor.golden | 16 - ...-interceptor_client_interceptors.go.golden | 21 ++ ...interceptor_interceptor_wrappers.go.golden | 13 + .../single-method-server-interceptor.golden | 16 - ...interceptor_interceptor_wrappers.go.golden | 13 + ...interceptor_service_interceptors.go.golden | 21 ++ .../single-service-server-interceptor.golden | 16 - ...interceptor_interceptor_wrappers.go.golden | 13 + ...interceptor_service_interceptors.go.golden | 28 ++ ...ead-payload_interceptor_wrappers.go.golden | 13 + ...ad-payload_service_interceptors.go.golden} | 15 +- ...read-result_interceptor_wrappers.go.golden | 13 + ...ead-result_service_interceptors.go.golden} | 15 +- .../streaming-interceptors.golden | 16 - ...nterceptors_interceptor_wrappers.go.golden | 13 + ...nterceptors_service_interceptors.go.golden | 21 ++ dsl/method_test.go | 24 +- expr/http_endpoint_test.go | 4 +- expr/method_test.go | 100 +++++++ grpc/codegen/example_cli.go | 29 +- grpc/codegen/example_cli_test.go | 6 +- grpc/codegen/templates/do_grpc_cli.go.tpl | 14 +- grpc/codegen/templates/parse_endpoint.go.tpl | 15 +- .../testdata/client-interceptors.golden | 30 ++ .../client-no-server-pkgpath.golden | 4 +- .../{ => testdata}/client-no-server.golden | 4 +- ...r-hosting-multiple-services-pkgpath.golden | 4 +- ...nt-server-hosting-multiple-services.golden | 4 +- ...rver-hosting-service-subset-pkgpath.golden | 4 +- ...lient-server-hosting-service-subset.golden | 4 +- grpc/codegen/testdata/dsls.go | 24 ++ http/codegen/client_cli.go | 7 + http/codegen/example_cli.go | 34 ++- http/codegen/templates/cli_end.go.tpl | 29 +- http/codegen/templates/cli_start.go.tpl | 10 + http/codegen/templates/parse_endpoint.go.tpl | 14 +- pkg/interceptor.go | 5 - 104 files changed, 2321 insertions(+), 712 deletions(-) create mode 100644 codegen/example/templates/server_interceptors.go.tpl create mode 100644 codegen/service/example_interceptors.go create mode 100644 codegen/service/example_interceptors_test.go create mode 100644 codegen/service/templates/client_interceptor_wrappers.go.tpl create mode 100644 codegen/service/templates/client_interceptors.go.tpl create mode 100644 codegen/service/templates/example_client_interceptor.go.tpl rename codegen/service/templates/{security_authfuncs.go.tpl => example_security_authfuncs.go.tpl} (100%) create mode 100644 codegen/service/templates/example_server_interceptor.go.tpl rename codegen/service/templates/{service_init.go.tpl => example_service_init.go.tpl} (100%) rename codegen/service/templates/{service_struct.go.tpl => example_service_struct.go.tpl} (100%) create mode 100644 codegen/service/templates/server_interceptor_wrappers.go.tpl create mode 100644 codegen/service/templates/server_interceptors.go.tpl create mode 100644 codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden create mode 100644 codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden create mode 100644 codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden create mode 100644 codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden create mode 100644 codegen/service/testdata/example_interceptors_dsls.go create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-read-payload.golden => interceptor-with-read-payload_service_interceptors.go.golden} (70%) create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-read-result.golden => interceptor-with-read-result_service_interceptors.go.golden} (69%) create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-read-write-payload.golden => interceptor-with-read-write-payload_service_interceptors.go.golden} (71%) create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-read-write-result.golden => interceptor-with-read-write-result_service_interceptors.go.golden} (71%) create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-write-payload.golden => interceptor-with-write-payload_service_interceptors.go.golden} (70%) create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{interceptor-with-write-result.golden => interceptor-with-write-result_service_interceptors.go.golden} (69%) delete mode 100644 codegen/service/testdata/interceptors/multiple-interceptors.golden create mode 100644 codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden create mode 100644 codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden delete mode 100644 codegen/service/testdata/interceptors/single-api-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden create mode 100644 codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden delete mode 100644 codegen/service/testdata/interceptors/single-client-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden delete mode 100644 codegen/service/testdata/interceptors/single-method-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden create mode 100644 codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden delete mode 100644 codegen/service/testdata/interceptors/single-service-server-interceptor.golden create mode 100644 codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden create mode 100644 codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{streaming-interceptors-with-read-payload.golden => streaming-interceptors-with-read-payload_service_interceptors.go.golden} (71%) create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden rename codegen/service/testdata/interceptors/{streaming-interceptors-with-read-result.golden => streaming-interceptors-with-read-result_service_interceptors.go.golden} (70%) delete mode 100644 codegen/service/testdata/interceptors/streaming-interceptors.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden create mode 100644 codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden create mode 100644 grpc/codegen/testdata/client-interceptors.golden rename grpc/codegen/{ => testdata}/client-no-server-pkgpath.golden (91%) rename grpc/codegen/{ => testdata}/client-no-server.golden (91%) rename grpc/codegen/{ => testdata}/client-server-hosting-multiple-services-pkgpath.golden (92%) rename grpc/codegen/{ => testdata}/client-server-hosting-multiple-services.golden (91%) rename grpc/codegen/{ => testdata}/client-server-hosting-service-subset-pkgpath.golden (92%) rename grpc/codegen/{ => testdata}/client-server-hosting-service-subset.golden (91%) diff --git a/codegen/cli/cli.go b/codegen/cli/cli.go index a69f747036..1135424ae3 100644 --- a/codegen/cli/cli.go +++ b/codegen/cli/cli.go @@ -34,6 +34,8 @@ type ( // PkgName is the service HTTP client package import name, // e.g. "storagec". PkgName string + // Interceptors contains the data for client interceptors if any. + Interceptors *InterceptorData } // SubcommandData contains the data needed to render a sub-command. @@ -58,6 +60,14 @@ type ( Example string } + // InterceptorData contains the data needed to generate interceptor code. + InterceptorData struct { + // VarName is the name of the interceptor variable. + VarName string + // PkgName is the package name containing the interceptor type. + PkgName string + } + // FlagData contains the data needed to render a command-line flag. FlagData struct { // Name is the name of the flag, e.g. "list-vintage" @@ -151,11 +161,21 @@ func BuildCommandData(data *service.Data) *CommandData { if description == "" { description = fmt.Sprintf("Make requests to the %q service", data.Name) } + + var interceptors *InterceptorData + if len(data.ClientInterceptors) > 0 { + interceptors = &InterceptorData{ + VarName: "inter", + PkgName: data.PkgName, + } + } + return &CommandData{ - Name: codegen.KebabCase(data.Name), - VarName: codegen.Goify(data.Name, false), - Description: description, - PkgName: data.PkgName + "c", + Name: codegen.KebabCase(data.Name), + VarName: codegen.Goify(data.Name, false), + Description: description, + PkgName: data.PkgName + "c", + Interceptors: interceptors, } } diff --git a/codegen/example/example_server.go b/codegen/example/example_server.go index f896106eb4..35576abf21 100644 --- a/codegen/example/example_server.go +++ b/codegen/example/example_server.go @@ -50,14 +50,17 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c // Iterate through services listed in the server expression. svcData := make([]*service.Data, len(svr.Services)) scope := codegen.NewNameScope() + hasInterceptors := false for i, svc := range svr.Services { sd := service.Services.Get(svc) svcData[i] = sd specs = append(specs, &codegen.ImportSpec{ Path: path.Join(genpkg, sd.PathName), - Name: scope.Unique(sd.PkgName), + Name: scope.Unique(sd.PkgName, "svc"), }) + hasInterceptors = hasInterceptors || len(sd.ServerInterceptors) > 0 } + interPkg := scope.Unique("interceptors", "ex") var ( rootPath string @@ -73,6 +76,9 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api") } specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg}) + if hasInterceptors { + specs = append(specs, &codegen.ImportSpec{Path: path.Join(rootPath, "interceptors"), Name: interPkg}) + } sections := []*codegen.SectionTemplate{ codegen.Header("", "main", specs), @@ -101,6 +107,18 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c FuncMap: map[string]any{ "mustInitServices": mustInitServices, }, + }, { + Name: "server-main-interceptors", + Source: readTemplate("server_interceptors"), + Data: map[string]any{ + "APIPkg": apiPkg, + "InterPkg": interPkg, + "Services": svcData, + "HasInterceptors": hasInterceptors, + }, + FuncMap: map[string]any{ + "mustInitServices": mustInitServices, + }, }, { Name: "server-main-endpoints", Source: readTemplate("server_endpoints"), diff --git a/codegen/example/templates/server_endpoints.go.tpl b/codegen/example/templates/server_endpoints.go.tpl index 3916ab3bd4..97be6436ee 100644 --- a/codegen/example/templates/server_endpoints.go.tpl +++ b/codegen/example/templates/server_endpoints.go.tpl @@ -11,7 +11,7 @@ { {{- range .Services }} {{- if .Methods }} - {{ .VarName }}Endpoints = {{ .PkgName }}.NewEndpoints({{ .VarName }}Svc) + {{ .VarName }}Endpoints = {{ .PkgName }}.NewEndpoints({{ .VarName }}Svc{{ if .ServerInterceptors }}, {{ .VarName }}Interceptors{{ end }}) {{ .VarName }}Endpoints.Use(debug.LogPayloads()) {{ .VarName }}Endpoints.Use(log.Endpoint) {{- end }} diff --git a/codegen/example/templates/server_interceptors.go.tpl b/codegen/example/templates/server_interceptors.go.tpl new file mode 100644 index 0000000000..c8bf2215b7 --- /dev/null +++ b/codegen/example/templates/server_interceptors.go.tpl @@ -0,0 +1,23 @@ +{{- if mustInitServices .Services }} + {{- if .HasInterceptors }} + {{ comment "Initialize the interceptors." }} + var ( + {{- range .Services }} + {{- if and .Methods .ServerInterceptors }} + {{ .VarName }}Interceptors {{ .PkgName }}.ServerInterceptors + {{- end }} + {{- end }} + ) + {{- end }} + {{- if .HasInterceptors }} + { + {{- end }} + {{- range .Services }} + {{- if and .Methods .ServerInterceptors }} + {{ .VarName }}Interceptors = {{ $.InterPkg }}.New{{ .StructName }}ServerInterceptors() + {{- end }} + {{- end }} + {{- if .HasInterceptors }} + } + {{- end }} +{{- end }} \ No newline at end of file diff --git a/codegen/generator/example.go b/codegen/generator/example.go index c78545a3f7..827880149f 100644 --- a/codegen/generator/example.go +++ b/codegen/generator/example.go @@ -25,6 +25,11 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) { files = append(files, fs...) } + // example interceptors implementation + if fs := service.ExampleInterceptorsFiles(genpkg, r); len(fs) != 0 { + files = append(files, fs...) + } + // server main if fs := example.ServerFiles(genpkg, r); len(fs) != 0 { files = append(files, fs...) @@ -54,6 +59,8 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) { files = append(files, fs...) } } + + // Add imports defined via struct:field:type for _, f := range files { if len(f.SectionTemplates) > 0 { for _, s := range r.Services { diff --git a/codegen/service/client.go b/codegen/service/client.go index f991e81aed..a088de0d37 100644 --- a/codegen/service/client.go +++ b/codegen/service/client.go @@ -45,18 +45,6 @@ func ClientFile(_ string, service *expr.ServiceExpr) *codegen.File { Source: readTemplate("service_client_method"), Data: m, }) - if len(m.ClientInterceptors) > 0 { - sections = append(sections, &codegen.SectionTemplate{ - Name: "client-wrapper", - Source: readTemplate("client_wrappers"), - Data: map[string]interface{}{ - "Method": m.Name, - "MethodVarName": codegen.Goify(m.Name, true), - "Service": svc.Name, - "ClientInterceptors": m.ClientInterceptors, - }, - }) - } } } diff --git a/codegen/service/endpoint.go b/codegen/service/endpoint.go index 45d476620e..b499a9e6f7 100644 --- a/codegen/service/endpoint.go +++ b/codegen/service/endpoint.go @@ -25,15 +25,17 @@ type ( ServiceVarName string // Methods lists the endpoint struct methods. Methods []*EndpointMethodData - // HasServerInterceptors indicates if the service has server interceptors. - HasServerInterceptors bool - // HasClientInterceptors indicates if the service has client interceptors. - HasClientInterceptors bool // ClientInitArgs lists the arguments needed to instantiate the client. ClientInitArgs string // Schemes contains the security schemes types used by the // all the endpoints. Schemes SchemesData + // HasServerInterceptors indicates that the service has server-side + // interceptors. + HasServerInterceptors bool + // HasClientInterceptors indicates that the service has client-side + // interceptors. + HasClientInterceptors bool } // EndpointMethodData describes a single endpoint method. @@ -130,18 +132,6 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File { Data: m, FuncMap: map[string]any{"payloadVar": payloadVar}, }) - if len(m.ServerInterceptors) > 0 { - sections = append(sections, &codegen.SectionTemplate{ - Name: "endpoint-wrapper", - Source: readTemplate("endpoint_wrappers"), - Data: map[string]interface{}{ - "MethodVarName": codegen.Goify(m.Name, true), - "Method": m.Name, - "Service": svc.Name, - "ServerInterceptors": m.ServerInterceptors, - }, - }) - } } } @@ -153,34 +143,18 @@ func endpointData(service *expr.ServiceExpr) *EndpointsData { methods := make([]*EndpointMethodData, len(svc.Methods)) names := make([]string, len(svc.Methods)) for i, m := range svc.Methods { - serverInts, clientInts := buildMethodInterceptors(service.Method(m.Name), svc.Scope) methods[i] = &EndpointMethodData{ MethodData: m, ArgName: codegen.Goify(m.VarName, false), ServiceName: svc.Name, ServiceVarName: serviceInterfaceName, ClientVarName: clientStructName, - ServerInterceptors: serverInts, - ClientInterceptors: clientInts, + ServerInterceptors: m.ServerInterceptors, + ClientInterceptors: m.ClientInterceptors, } names[i] = codegen.Goify(m.VarName, false) } desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, service.Name) - var hasServerInterceptors, hasClientInterceptors bool - for _, m := range methods { - if len(m.ServerInterceptors) > 0 { - hasServerInterceptors = true - if hasClientInterceptors { - break - } - } - if len(m.ClientInterceptors) > 0 { - hasClientInterceptors = true - if hasServerInterceptors { - break - } - } - } return &EndpointsData{ Name: service.Name, Description: desc, @@ -189,9 +163,9 @@ func endpointData(service *expr.ServiceExpr) *EndpointsData { ServiceVarName: serviceInterfaceName, ClientInitArgs: strings.Join(names, ", "), Methods: methods, - HasServerInterceptors: hasServerInterceptors, - HasClientInterceptors: hasClientInterceptors, Schemes: svc.Schemes, + HasServerInterceptors: len(svc.ServerInterceptors) > 0, + HasClientInterceptors: len(svc.ClientInterceptors) > 0, } } diff --git a/codegen/service/example_interceptors.go b/codegen/service/example_interceptors.go new file mode 100644 index 0000000000..c348beae57 --- /dev/null +++ b/codegen/service/example_interceptors.go @@ -0,0 +1,86 @@ +package service + +import ( + "fmt" + "os" + "path" + "path/filepath" + + "goa.design/goa/v3/codegen" + "goa.design/goa/v3/expr" +) + +// ExampleInterceptorsFiles returns the files for the example server and client interceptors. +func ExampleInterceptorsFiles(genpkg string, r *expr.RootExpr) []*codegen.File { + var fw []*codegen.File + for _, svc := range r.Services { + if f := exampleInterceptorsFile(genpkg, svc); f != nil { + fw = append(fw, f...) + } + } + return fw +} + +// exampleInterceptorsFile returns the example interceptors for the given service. +func exampleInterceptorsFile(genpkg string, svc *expr.ServiceExpr) []*codegen.File { + sdata := Services.Get(svc.Name) + data := map[string]any{ + "ServiceName": sdata.Name, + "StructName": sdata.StructName, + "PkgName": "interceptors", + "ServerInterceptors": sdata.ServerInterceptors, + "ClientInterceptors": sdata.ClientInterceptors, + } + + var files []*codegen.File + + // Generate server interceptor if needed and file doesn't exist + if len(sdata.ServerInterceptors) > 0 { + serverPath := filepath.Join("interceptors", sdata.PathName+"_server.go") + if _, err := os.Stat(serverPath); os.IsNotExist(err) { + files = append(files, &codegen.File{ + Path: serverPath, + SectionTemplates: []*codegen.SectionTemplate{ + codegen.Header(fmt.Sprintf("%s example server interceptors", sdata.Name), "interceptors", []*codegen.ImportSpec{ + {Path: "context"}, + {Path: "fmt"}, + {Path: "goa.design/clue/log"}, + codegen.GoaImport(""), + {Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName}, + }), + { + Name: "server-interceptor", + Source: readTemplate("example_server_interceptor"), + Data: data, + }, + }, + }) + } + } + + // Generate client interceptor if needed and file doesn't exist + if len(sdata.ClientInterceptors) > 0 { + clientPath := filepath.Join("interceptors", sdata.PathName+"_client.go") + if _, err := os.Stat(clientPath); os.IsNotExist(err) { + files = append(files, &codegen.File{ + Path: clientPath, + SectionTemplates: []*codegen.SectionTemplate{ + codegen.Header(fmt.Sprintf("%s example client interceptors", sdata.Name), "interceptors", []*codegen.ImportSpec{ + {Path: "context"}, + {Path: "fmt"}, + {Path: "goa.design/clue/log"}, + codegen.GoaImport(""), + {Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName}, + }), + { + Name: "client-interceptor", + Source: readTemplate("example_client_interceptor"), + Data: data, + }, + }, + }) + } + } + + return files +} diff --git a/codegen/service/example_interceptors_test.go b/codegen/service/example_interceptors_test.go new file mode 100644 index 0000000000..37b8aa4207 --- /dev/null +++ b/codegen/service/example_interceptors_test.go @@ -0,0 +1,119 @@ +package service + +import ( + "bytes" + "go/format" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "goa.design/goa/v3/codegen/service/testdata" + "goa.design/goa/v3/expr" +) + +func TestExampleInterceptorsFiles(t *testing.T) { + cases := []struct { + Name string + DSL func() + ExpectedFiles []string + }{ + { + Name: "no-interceptors", + DSL: testdata.NoInterceptorExampleDSL, + }, + { + Name: "server-interceptor", + DSL: testdata.ServerInterceptorExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "server_interceptor_service_server.go"), + }, + }, + { + Name: "client-interceptor", + DSL: testdata.ClientInterceptorExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "client_interceptor_service_client.go"), + }, + }, + { + Name: "server-interceptor-by-name", + DSL: testdata.ServerInterceptorByNameExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "server_interceptor_by_name_service_server.go"), + }, + }, + { + Name: "multiple-interceptors", + DSL: testdata.MultipleInterceptorsExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "multiple_interceptors_service_server.go"), + filepath.Join("interceptors", "multiple_interceptors_service_client.go"), + }, + }, + { + Name: "multiple-services", + DSL: testdata.MultipleServicesInterceptorsExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "multiple_services_interceptors_service_server.go"), + filepath.Join("interceptors", "multiple_services_interceptors_service_client.go"), + filepath.Join("interceptors", "multiple_services_interceptors_service2_server.go"), + filepath.Join("interceptors", "multiple_services_interceptors_service2_client.go"), + }, + }, + { + Name: "api-interceptors", + DSL: testdata.APIInterceptorExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "api_interceptor_service_server.go"), + filepath.Join("interceptors", "api_interceptor_service_client.go"), + }, + }, + { + Name: "chained-interceptors", + DSL: testdata.ChainedInterceptorExampleDSL, + ExpectedFiles: []string{ + filepath.Join("interceptors", "chained_interceptor_service_server.go"), + filepath.Join("interceptors", "chained_interceptor_service_client.go"), + }, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + // Run DSL + root := expr.RunDSL(t, c.DSL) + require.NotNil(t, root) + + // Generate files + fs := ExampleInterceptorsFiles("", root) + require.Len(t, fs, len(c.ExpectedFiles)) + + // Verify file paths + paths := make([]string, len(fs)) + for i, f := range fs { + paths[i] = f.Path + } + assert.ElementsMatch(t, c.ExpectedFiles, paths) + + // Verify file content + for _, f := range fs { + buf := new(bytes.Buffer) + for _, s := range f.SectionTemplates { + require.NoError(t, s.Write(buf)) + } + bs, err := format.Source(buf.Bytes()) + require.NoError(t, err, buf.String()) + code := string(bs) + + // Use the base name of the generated file without extension for the golden file + baseName := filepath.Base(f.Path) + ext := filepath.Ext(baseName) + goldenName := baseName[:len(baseName)-len(ext)] + ".golden" + golden := filepath.Join("testdata", "example_interceptors", goldenName) + compareOrUpdateGolden(t, code, golden) + } + }) + } +} diff --git a/codegen/service/example_svc.go b/codegen/service/example_svc.go index 7f4b156054..46b977eff1 100644 --- a/codegen/service/example_svc.go +++ b/codegen/service/example_svc.go @@ -78,18 +78,18 @@ func exampleServiceFile(genpkg string, _ *expr.RootExpr, svc *expr.ServiceExpr, codegen.Header("", apipkg, specs), { Name: "basic-service-struct", - Source: readTemplate("service_struct"), + Source: readTemplate("example_service_struct"), Data: data, }, { Name: "basic-service-init", - Source: readTemplate("service_init"), + Source: readTemplate("example_service_init"), Data: data, }, } if len(data.Schemes) > 0 { sections = append(sections, &codegen.SectionTemplate{ Name: "security-authfuncs", - Source: readTemplate("security_authfuncs"), + Source: readTemplate("example_security_authfuncs"), Data: data, }) } diff --git a/codegen/service/interceptors.go b/codegen/service/interceptors.go index a383ddbb24..78ddb543ca 100644 --- a/codegen/service/interceptors.go +++ b/codegen/service/interceptors.go @@ -7,175 +7,144 @@ import ( "goa.design/goa/v3/expr" ) -type ( - // ServiceInterceptorData contains all data needed for generating interceptor code - ServiceInterceptorData struct { - Service string - PkgName string - Methods []*MethodInterceptorData - ServerInterceptors []*InterceptorData - ClientInterceptors []*InterceptorData - AllInterceptors []*InterceptorData - HasPrivateImplementationTypes bool - } +// InterceptorsFiles returns the interceptors files for the given service. +func InterceptorsFiles(genpkg string, service *expr.ServiceExpr) []*codegen.File { + var files []*codegen.File + svc := Services.Get(service.Name) - // MethodInterceptorData contains interceptor data for a single method - MethodInterceptorData struct { - Service string - Method string - MethodVarName string - PayloadRef string - ResultRef string - ServerInterceptors []*InterceptorData - ClientInterceptors []*InterceptorData + // Generate service-specific interceptor files + if len(svc.ServerInterceptors) > 0 { + files = append(files, interceptorFile(svc, true)) } - - // InterceptorData describes a single interceptor. - InterceptorData struct { - Name string - DesignName string - UnexportedName string - Description string - PayloadRef string - ResultRef string - ReadPayload []*AttributeData - WritePayload []*AttributeData - ReadResult []*AttributeData - WriteResult []*AttributeData - ServerStreamInputStruct string - ClientStreamInputStruct string + if len(svc.ClientInterceptors) > 0 { + files = append(files, interceptorFile(svc, false)) } - // AttributeData describes a single attribute. - AttributeData struct { - Name string - TypeRef string - FieldPointer bool + // Generate wrapper file if this service has any interceptors + if len(svc.ServerInterceptors) > 0 || len(svc.ClientInterceptors) > 0 { + files = append(files, wrapperFile(svc)) } -) -// InterceptorsFile returns the interceptors file for the given service. -func InterceptorsFile(genpkg string, service *expr.ServiceExpr) *codegen.File { - svc := Services.Get(service.Name) - data := interceptorsData(service) - if len(data.ServerInterceptors) == 0 && len(data.ClientInterceptors) == 0 { - return nil + return files +} + +// interceptorFile returns the file defining the interceptors. +func interceptorFile(svc *Data, server bool) *codegen.File { + filename := "client_interceptors.go" + template := "client_interceptors" + section := "client-interceptors" + desc := "Client Interceptors" + var data []*InterceptorData + if server { + filename = "service_interceptors.go" + template = "server_interceptors" + section = "server-interceptors" + desc = "Server Interceptors" + data = svc.ServerInterceptors + } + desc = svc.Name + desc + path := filepath.Join(codegen.Gendir, svc.PathName, filename) + + if !server { + // We don't want to generate duplicate interceptor info data structures for + // interceptors that are both server and client side. + serverInterceptors := make(map[string]struct{}, len(svc.ServerInterceptors)) + for _, intr := range svc.ServerInterceptors { + serverInterceptors[intr.Name] = struct{}{} + } + for _, intr := range svc.ClientInterceptors { + if _, ok := serverInterceptors[intr.Name]; ok { + continue + } + data = append(data, intr) + } } - path := filepath.Join(codegen.Gendir, svc.PathName, "interceptors.go") sections := []*codegen.SectionTemplate{ - codegen.Header(service.Name+" interceptors", svc.PkgName, []*codegen.ImportSpec{ + codegen.Header(desc, svc.PkgName, []*codegen.ImportSpec{ {Path: "context"}, codegen.GoaImport(""), }), { - Name: "interceptors", + Name: "section" + "-struct", + Source: readTemplate(template), + Data: svc, + }, + } + if len(data) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: section, Source: readTemplate("interceptors"), Data: data, - }, + FuncMap: map[string]any{ + "hasPrivateImplementationTypes": hasPrivateImplementationTypes, + }, + }) } - return &codegen.File{Path: path, SectionTemplates: sections} -} - -func interceptorsData(service *expr.ServiceExpr) *ServiceInterceptorData { - svc := Services.Get(service.Name) - scope := svc.Scope - - // Build method data first - methods := make([]*MethodInterceptorData, 0, len(service.Methods)) - seenInts := make(map[string]*InterceptorData) - var serviceServerInts, serviceClientInts, allInts []*InterceptorData - var hasTypes bool - - for _, m := range service.Methods { - methodServerInts, methodClientInts := buildMethodInterceptors(m, scope) - if len(methodServerInts) == 0 && len(methodClientInts) == 0 { + // Add wrapper sections for each method that has interceptors + for _, m := range svc.Methods { + if server && len(m.ServerInterceptors) == 0 || !server && len(m.ClientInterceptors) == 0 { continue } - hasTypes = hasTypes || hasPrivateImplementationTypes(methodServerInts) || hasPrivateImplementationTypes(methodClientInts) - - // Add method data - methods = append(methods, &MethodInterceptorData{ - Service: svc.Name, - Method: m.Name, - MethodVarName: codegen.Goify(m.Name, true), - PayloadRef: scope.GoFullTypeRef(m.Payload, ""), - ResultRef: scope.GoFullTypeRef(m.Result, ""), - ServerInterceptors: methodServerInts, - ClientInterceptors: methodClientInts, - }) - - // Collect unique interceptors - for _, i := range methodServerInts { - if _, ok := seenInts[i.Name]; !ok { - seenInts[i.Name] = i - serviceServerInts = append(serviceServerInts, i) - allInts = append(allInts, i) - } + template := "endpoint_wrappers" + templateName := "endpoint-wrapper" + if !server { + template = "client_wrappers" + templateName = "client-wrapper" } - for _, i := range methodClientInts { - if _, ok := seenInts[i.Name]; !ok { - seenInts[i.Name] = i - serviceClientInts = append(serviceClientInts, i) - allInts = append(allInts, i) - } - } - } - - return &ServiceInterceptorData{ - Service: service.Name, - PkgName: svc.PkgName, - Methods: methods, - ServerInterceptors: serviceServerInts, - ClientInterceptors: serviceClientInts, - AllInterceptors: allInts, - HasPrivateImplementationTypes: hasTypes, + sections = append(sections, &codegen.SectionTemplate{ + Name: templateName, + Source: readTemplate(template), + Data: map[string]interface{}{ + "MethodVarName": codegen.Goify(m.Name, true), + "Method": m.Name, + "Service": svc.Name, + "ServerInterceptors": m.ServerInterceptors, + "ClientInterceptors": m.ClientInterceptors, + }, + }) } + return &codegen.File{Path: path, SectionTemplates: sections} } -func buildMethodInterceptors(m *expr.MethodExpr, scope *codegen.NameScope) ([]*InterceptorData, []*InterceptorData) { - svc := Services.Get(m.Service.Name) - methodData := svc.Method(m.Name) - var serverEndpointStruct, clientEndpointStruct string - if methodData.ServerStream != nil { - serverEndpointStruct = methodData.ServerStream.EndpointStruct - } - if methodData.ClientStream != nil { - clientEndpointStruct = methodData.ClientStream.EndpointStruct - } - var hasPrivateImplementationTypes bool - buildInterceptor := func(intr *expr.InterceptorExpr) *InterceptorData { - hasPrivateImplementationTypes = hasPrivateImplementationTypes || - intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil - - return &InterceptorData{ - Name: codegen.Goify(intr.Name, true), - DesignName: intr.Name, - UnexportedName: codegen.Goify(intr.Name, false), - Description: intr.Description, - PayloadRef: methodData.PayloadRef, - ResultRef: methodData.ResultRef, - ServerStreamInputStruct: serverEndpointStruct, - ClientStreamInputStruct: clientEndpointStruct, - ReadPayload: collectAttributes(intr.ReadPayload, m.Payload, scope), - WritePayload: collectAttributes(intr.WritePayload, m.Payload, scope), - ReadResult: collectAttributes(intr.ReadResult, m.Result, scope), - WriteResult: collectAttributes(intr.WriteResult, m.Result, scope), - } +// wrapperFile returns the file containing the interceptor wrappers. +func wrapperFile(svc *Data) *codegen.File { + path := filepath.Join(codegen.Gendir, svc.PathName, "interceptor_wrappers.go") + + var sections []*codegen.SectionTemplate + sections = append(sections, codegen.Header("Interceptor wrappers", svc.PkgName, []*codegen.ImportSpec{ + {Path: "context"}, + {Path: "fmt"}, + codegen.GoaImport(""), + })) + + // Generate the interceptor wrapper functions first (only once) + if len(svc.ServerInterceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "server-interceptor-wrappers", + Source: readTemplate("server_interceptor_wrappers"), + Data: map[string]interface{}{ + "Service": svc.Name, + "ServerInterceptors": svc.ServerInterceptors, + }, + }) } - - serverInts := make([]*InterceptorData, len(m.ServerInterceptors)) - for i, intr := range m.ServerInterceptors { - serverInts[i] = buildInterceptor(intr) + if len(svc.ClientInterceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "client-interceptor-wrappers", + Source: readTemplate("client_interceptor_wrappers"), + Data: map[string]interface{}{ + "Service": svc.Name, + "ClientInterceptors": svc.ClientInterceptors, + }, + }) } - clientInts := make([]*InterceptorData, len(m.ClientInterceptors)) - for i, intr := range m.ClientInterceptors { - clientInts[i] = buildInterceptor(intr) + return &codegen.File{ + Path: path, + SectionTemplates: sections, } - - return serverInts, clientInts } // hasPrivateImplementationTypes returns true if any of the interceptors have @@ -188,30 +157,3 @@ func hasPrivateImplementationTypes(interceptors []*InterceptorData) bool { } return false } - -// collectAttributes builds AttributeData from an AttributeExpr -func collectAttributes(attrNames, parent *expr.AttributeExpr, scope *codegen.NameScope) []*AttributeData { - if attrNames == nil { - return nil - } - - obj := expr.AsObject(attrNames.Type) - if obj == nil { - return nil - } - - data := make([]*AttributeData, len(*obj)) - for i, nat := range *obj { - parentAttr := parent.Find(nat.Name) - if parentAttr == nil { - continue - } - - data[i] = &AttributeData{ - Name: codegen.Goify(nat.Name, true), - TypeRef: scope.GoTypeRef(parentAttr), - FieldPointer: parent.IsPrimitivePointer(nat.Name, true), - } - } - return data -} diff --git a/codegen/service/interceptors_test.go b/codegen/service/interceptors_test.go index 0817ac2b31..e484535c9a 100644 --- a/codegen/service/interceptors_test.go +++ b/codegen/service/interceptors_test.go @@ -26,49 +26,46 @@ func init() { func TestInterceptors(t *testing.T) { cases := []struct { - Name string - DSL func() + Name string + DSL func() + expectedFileCount int }{ - {"no-interceptors", testdata.NoInterceptorsDSL}, - {"single-api-server-interceptor", testdata.SingleAPIServerInterceptorDSL}, - {"single-service-server-interceptor", testdata.SingleServiceServerInterceptorDSL}, - {"single-method-server-interceptor", testdata.SingleMethodServerInterceptorDSL}, - {"single-client-interceptor", testdata.SingleClientInterceptorDSL}, - {"multiple-interceptors", testdata.MultipleInterceptorsDSL}, - {"interceptor-with-read-payload", testdata.InterceptorWithReadPayloadDSL}, - {"interceptor-with-write-payload", testdata.InterceptorWithWritePayloadDSL}, - {"interceptor-with-read-write-payload", testdata.InterceptorWithReadWritePayloadDSL}, - {"interceptor-with-read-result", testdata.InterceptorWithReadResultDSL}, - {"interceptor-with-write-result", testdata.InterceptorWithWriteResultDSL}, - {"interceptor-with-read-write-result", testdata.InterceptorWithReadWriteResultDSL}, - {"streaming-interceptors", testdata.StreamingInterceptorsDSL}, - {"streaming-interceptors-with-read-payload", testdata.StreamingInterceptorsWithReadPayloadDSL}, - {"streaming-interceptors-with-read-result", testdata.StreamingInterceptorsWithReadResultDSL}, + {"no-interceptors", testdata.NoInterceptorsDSL, 0}, + {"single-api-server-interceptor", testdata.SingleAPIServerInterceptorDSL, 2}, + {"single-service-server-interceptor", testdata.SingleServiceServerInterceptorDSL, 2}, + {"single-method-server-interceptor", testdata.SingleMethodServerInterceptorDSL, 2}, + {"single-client-interceptor", testdata.SingleClientInterceptorDSL, 2}, + {"multiple-interceptors", testdata.MultipleInterceptorsExampleDSL, 3}, + {"interceptor-with-read-payload", testdata.InterceptorWithReadPayloadDSL, 3}, + {"interceptor-with-write-payload", testdata.InterceptorWithWritePayloadDSL, 3}, + {"interceptor-with-read-write-payload", testdata.InterceptorWithReadWritePayloadDSL, 3}, + {"interceptor-with-read-result", testdata.InterceptorWithReadResultDSL, 3}, + {"interceptor-with-write-result", testdata.InterceptorWithWriteResultDSL, 3}, + {"interceptor-with-read-write-result", testdata.InterceptorWithReadWriteResultDSL, 3}, + {"streaming-interceptors", testdata.StreamingInterceptorsDSL, 2}, + {"streaming-interceptors-with-read-payload", testdata.StreamingInterceptorsWithReadPayloadDSL, 2}, + {"streaming-interceptors-with-read-result", testdata.StreamingInterceptorsWithReadResultDSL, 2}, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { root := runDSL(t, c.DSL) require.Len(t, root.Services, 1) - fs := InterceptorsFile("goa.design/goa/example", root.Services[0]) + fs := InterceptorsFiles("goa.design/goa/example", root.Services[0]) - if c.Name == "no-interceptors" { - assert.Nil(t, fs) - return - } - - require.NotNil(t, fs) + require.Len(t, fs, c.expectedFileCount) + for _, f := range fs { + buf := new(bytes.Buffer) + for _, s := range f.SectionTemplates[1:] { + require.NoError(t, s.Write(buf)) + } + bs, err := format.Source(buf.Bytes()) + require.NoError(t, err, buf.String()) + code := strings.ReplaceAll(string(bs), "\r\n", "\n") - buf := new(bytes.Buffer) - for _, s := range fs.SectionTemplates[1:] { - require.NoError(t, s.Write(buf)) + golden := filepath.Join("testdata", "interceptors", c.Name+"_"+filepath.Base(f.Path)+".golden") + compareOrUpdateGolden(t, code, golden) } - bs, err := format.Source(buf.Bytes()) - require.NoError(t, err, buf.String()) - code := strings.ReplaceAll(string(bs), "\r\n", "\n") - - golden := filepath.Join("testdata", "interceptors", c.Name+".golden") - compareOrUpdateGolden(t, code, golden) }) } } diff --git a/codegen/service/service.go b/codegen/service/service.go index a6c45448d1..5fc68eb860 100644 --- a/codegen/service/service.go +++ b/codegen/service/service.go @@ -195,11 +195,9 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s } files := []*codegen.File{{Path: svcPath, SectionTemplates: sections}} - // interceptor.go - if file := InterceptorsFile(genpkg, service); file != nil { - files = append(files, file) - } - + // service and client interceptors + files = append(files, InterceptorsFiles(genpkg, service)...) + // user types paths := make([]string, len(typeDefSections)) i := 0 diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go index f0a62f81cb..9451d05df7 100644 --- a/codegen/service/service_data.go +++ b/codegen/service/service_data.go @@ -37,12 +37,6 @@ type ( // ServicesData encapsulates the data computed from the service designs. ServicesData map[string]*Data - // RequirementsData is the list of security requirements. - RequirementsData []*RequirementData - - // SchemesData is the list of security schemes. - SchemesData []*SchemeData - // Data contains the data used to render the code related to a single // service. Data struct { @@ -70,6 +64,12 @@ type ( Methods []*MethodData // Schemes is the list of security schemes required by the service methods. Schemes SchemesData + // ServerInterceptors is the union of the server interceptors defined + // at the methods, service and the API level. + ServerInterceptors []*InterceptorData + // ClientInterceptors is the union of the client interceptors defined + // at the methods, service and the API level. + ClientInterceptors []*InterceptorData // Scope initialized with all the service types. Scope *codegen.NameScope // ViewScope initialized with all the viewed types. @@ -99,38 +99,6 @@ type ( unionValueMethods []*UnionValueMethodData } - // UnionValueMethodData describes a method used on a union value type. - UnionValueMethodData struct { - // Name is the name of the function. - Name string - // TypeRef is a reference on the target union value type. - TypeRef string - // Loc defines the file and Go package of the method if - // overridden in corresponding union type via Meta. - Loc *codegen.Location - } - - // ErrorInitData describes an error returned by a service method of type - // ErrorResult. - ErrorInitData struct { - // Name is the name of the init function. - Name string - // Description is the error description. - Description string - // ErrName is the name of the error. - ErrName string - // TypeName is the error struct type name. - TypeName string - // TypeRef is the reference to the error type. - TypeRef string - // Temporary indicates whether the error is temporary. - Temporary bool - // Timeout indicates whether the error is due to timeouts. - Timeout bool - // Fault indicates whether the error is server-side fault. - Fault bool - } - // MethodData describes a single service method. MethodData struct { // Name is the method name. @@ -188,6 +156,12 @@ type ( // Schemes contains the security schemes types used by the // method. Schemes SchemesData + // ServerInterceptors list the server interceptors that apply to this + // method. + ServerInterceptors []*InterceptorData + // ClientInterceptors list the client interceptors that apply to this + // method. + ClientInterceptors []*InterceptorData // ViewedResult contains the data required to generate the code handling // views if any. ViewedResult *ViewedResultTypeData @@ -252,6 +226,81 @@ type ( Kind expr.StreamKind } + // ErrorInitData describes an error returned by a service method of type + // ErrorResult. + ErrorInitData struct { + // Name is the name of the init function. + Name string + // Description is the error description. + Description string + // ErrName is the name of the error. + ErrName string + // TypeName is the error struct type name. + TypeName string + // TypeRef is the reference to the error type. + TypeRef string + // Temporary indicates whether the error is temporary. + Temporary bool + // Timeout indicates whether the error is due to timeouts. + Timeout bool + // Fault indicates whether the error is server-side fault. + Fault bool + } + + // InterceptorData describes a single interceptor that can read and write + // payload and result attributes. + InterceptorData struct { + // Name is the name of the interceptor used in the generated code. + Name string + // DesignName is the name of the interceptor as defined in the design. + DesignName string + // UnexportedName is the private version of the interceptor name used in + // generated code. + UnexportedName string + // Description is the description of the interceptor from the design. + Description string + // PayloadRef is the reference to the payload type that the interceptor + // can access. + PayloadRef string + // ResultRef is the reference to the result type that the interceptor + // can access. + ResultRef string + // ReadPayload lists the payload attributes that the interceptor can + // read. These are defined using ReadPayload() in the design DSL. + ReadPayload []*AttributeData + // WritePayload lists the payload attributes that the interceptor can + // write. These are defined using WritePayload() in the design DSL. + WritePayload []*AttributeData + // ReadResult lists the result attributes that the interceptor can read. + // These are defined using ReadResult() in the design DSL. + ReadResult []*AttributeData + // WriteResult lists the result attributes that the interceptor can + // write. These are defined using WriteResult() in the design DSL. + WriteResult []*AttributeData + // ServerStreamInputStruct is the name of the server stream input struct + // type used when the interceptor is applied to a streaming endpoint. + ServerStreamInputStruct string + // ClientStreamInputStruct is the name of the client stream input struct + // type used when the interceptor is applied to a streaming endpoint. + ClientStreamInputStruct string + } + + // AttributeData describes a single attribute. + AttributeData struct { + // Name is the name of the attribute. + Name string + // TypeRef is the reference to the attribute type. + TypeRef string + // FieldPointer is true if the attribute is a pointer. + FieldPointer bool + } + + // RequirementsData is the list of security requirements. + RequirementsData []*RequirementData + + // SchemesData is the list of security schemes. + SchemesData []*SchemeData + // RequirementData lists the schemes and scopes defined by a single // security requirement. RequirementData struct { @@ -280,6 +329,17 @@ type ( Type expr.UserType } + // UnionValueMethodData describes a method used on a union value type. + UnionValueMethodData struct { + // Name is the name of the function. + Name string + // TypeRef is a reference on the target union value type. + TypeRef string + // Loc defines the file and Go package of the method if + // overridden in corresponding union type via Meta. + Loc *codegen.Location + } + // SchemeData describes a single security scheme. SchemeData struct { // Kind is the type of scheme, one of "Basic", "APIKey", "JWT" @@ -746,6 +806,17 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { } } + var ( + serverInterceptors []*InterceptorData + clientInterceptors []*InterceptorData + seenServer = make(map[string]struct{}) + seenClient = make(map[string]struct{}) + ) + for _, m := range methods { + serverInterceptors = append(serverInterceptors, collectInterceptors(m.ServerInterceptors, seenServer)...) + clientInterceptors = append(clientInterceptors, collectInterceptors(m.ClientInterceptors, seenClient)...) + } + var ( desc string ) @@ -769,6 +840,8 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { ViewsPkg: viewspkg, Methods: methods, Schemes: schemes, + ServerInterceptors: serverInterceptors, + ClientInterceptors: clientInterceptors, Scope: scope, ViewScope: viewScope, errorTypes: errTypes, @@ -876,6 +949,18 @@ func collectUnionMethods(att *expr.AttributeExpr, scope *codegen.NameScope, loc return } +// collectInterceptors traverses the interceptors to build a unique set. +func collectInterceptors(interceptors []*InterceptorData, seen map[string]struct{}) (data []*InterceptorData) { + for _, i := range interceptors { + if _, ok := seen[i.Name]; ok { + continue + } + seen[i.Name] = struct{}{} + data = append(data, i) + } + return +} + // buildErrorInitData creates the data needed to generate code around endpoint error return values. func buildErrorInitData(er *expr.ErrorExpr, scope *codegen.NameScope) *ErrorInitData { _, temporary := er.AttributeExpr.Meta["goa:error:temporary"] @@ -1001,14 +1086,16 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData { RequestStruct: vname + "RequestData", ResponseStruct: vname + "ResponseData", } - if m.IsStreaming() { - initStreamData(data, m, vname, rname, resultRef, scope) - } + initStreamData(data, m, vname, rname, resultRef, scope) + initInterceptorsData(data, m, payloadRef, resultRef, scope) return data } // initStreamData initializes the streaming payload data structures and methods. func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRef string, scope *codegen.NameScope) { + if !m.IsStreaming() { + return + } var ( spayloadName string spayloadRef string @@ -1082,6 +1169,41 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe data.StreamingPayloadEx = spayloadEx } +func initInterceptorsData(data *MethodData, m *expr.MethodExpr, payloadRef, resultRef string, scope *codegen.NameScope) { + if len(m.ServerInterceptors) == 0 && len(m.ClientInterceptors) == 0 { + return + } + buildInterceptorData := func(i *expr.InterceptorExpr, serverStreamStruct, clientStreamStruct string, scope *codegen.NameScope) *InterceptorData { + return &InterceptorData{ + Name: codegen.Goify(i.Name, true), + DesignName: i.Name, + UnexportedName: codegen.Goify(i.Name, false), + Description: i.Description, + PayloadRef: payloadRef, + ResultRef: resultRef, + ServerStreamInputStruct: serverStreamStruct, + ClientStreamInputStruct: clientStreamStruct, + ReadPayload: collectAttributes(i.ReadPayload, m.Payload, scope), + WritePayload: collectAttributes(i.WritePayload, m.Payload, scope), + ReadResult: collectAttributes(i.ReadResult, m.Result, scope), + WriteResult: collectAttributes(i.WriteResult, m.Result, scope), + } + } + var serverEndpointStruct, clientEndpointStruct string + if data.ServerStream != nil { + serverEndpointStruct = data.ServerStream.EndpointStruct + } + if data.ClientStream != nil { + clientEndpointStruct = data.ClientStream.EndpointStruct + } + for _, i := range m.ServerInterceptors { + data.ServerInterceptors = append(data.ServerInterceptors, buildInterceptorData(i, serverEndpointStruct, clientEndpointStruct, scope)) + } + for _, i := range m.ClientInterceptors { + data.ClientInterceptors = append(data.ClientInterceptors, buildInterceptorData(i, serverEndpointStruct, clientEndpointStruct, scope)) + } +} + // BuildSchemeData builds the scheme data for the given scheme and method expr. func BuildSchemeData(s *expr.SchemeExpr, m *expr.MethodExpr) *SchemeData { if !expr.IsObject(m.Payload.Type) { @@ -1184,6 +1306,30 @@ func BuildSchemeData(s *expr.SchemeExpr, m *expr.MethodExpr) *SchemeData { return nil } +// collectAttributes builds AttributeData from an AttributeExpr +func collectAttributes(attrNames, parent *expr.AttributeExpr, scope *codegen.NameScope) []*AttributeData { + if attrNames == nil { + return nil + } + obj := expr.AsObject(attrNames.Type) + if obj == nil { + return nil + } + data := make([]*AttributeData, len(*obj)) + for i, nat := range *obj { + parentAttr := parent.Find(nat.Name) + if parentAttr == nil { + continue + } + data[i] = &AttributeData{ + Name: codegen.Goify(nat.Name, true), + TypeRef: scope.GoTypeRef(parentAttr), + FieldPointer: parent.IsPrimitivePointer(nat.Name, true), + } + } + return data +} + // collectProjectedTypes builds a projected type for every user type found // when recursing through the attributes. The projected types live in the views // package and support the marshaling and unmarshalling of result types that diff --git a/codegen/service/templates/client_interceptor_wrappers.go.tpl b/codegen/service/templates/client_interceptor_wrappers.go.tpl new file mode 100644 index 0000000000..f2f63223db --- /dev/null +++ b/codegen/service/templates/client_interceptor_wrappers.go.tpl @@ -0,0 +1,18 @@ +{{- range .ClientInterceptors }} +{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .DesignName) }} +func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &{{ .Name }}Info{ + Service: "{{ $.Service }}", + Method: method, + Endpoint: endpoint, + {{- if .ClientStreamInputStruct }} + RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + return i.{{ .Name }}(ctx, info, endpoint) + } +} +{{- end }} diff --git a/codegen/service/templates/client_interceptors.go.tpl b/codegen/service/templates/client_interceptors.go.tpl new file mode 100644 index 0000000000..9528e3d123 --- /dev/null +++ b/codegen/service/templates/client_interceptors.go.tpl @@ -0,0 +1,12 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { +{{- range .ClientInterceptors }} +{{- if .Description }} + {{ comment .Description }} +{{- end }} + {{ .Name }}(ctx context.Context, info *{{ .Name }}Info, next goa.Endpoint) (any, error) +{{- end }} +} diff --git a/codegen/service/templates/client_wrappers.go.tpl b/codegen/service/templates/client_wrappers.go.tpl index dd52071dd9..6b05597c00 100644 --- a/codegen/service/templates/client_wrappers.go.tpl +++ b/codegen/service/templates/client_wrappers.go.tpl @@ -5,25 +5,3 @@ func Wrap{{ .MethodVarName }}ClientEndpoint(endpoint goa.Endpoint, i ClientInter {{- end }} return endpoint } - -{{- range .ClientInterceptors }} -{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .DesignName) }} -func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &{{ .Name }}Info{ - Service: "{{ $.Service }}", - Method: method, - Endpoint: endpoint, - {{- if .ClientStreamInputStruct }} - RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload, - {{- else }} - RawPayload: req, - {{- end }} - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.{{ .Name }}(ctx, info, next) - } -} -{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/endpoint_wrappers.go.tpl b/codegen/service/templates/endpoint_wrappers.go.tpl index 63dcf39e1e..8c82741cc9 100644 --- a/codegen/service/templates/endpoint_wrappers.go.tpl +++ b/codegen/service/templates/endpoint_wrappers.go.tpl @@ -5,26 +5,3 @@ func Wrap{{ .MethodVarName }}Endpoint(endpoint goa.Endpoint, i ServerInterceptor {{- end }} return endpoint } - -{{- range .ServerInterceptors }} -{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .DesignName) }} -func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &{{ .Name }}Info{ - Service: "{{ $.Service }}", - Method: method, - Endpoint: endpoint, - {{- if .ServerStreamInputStruct }} - RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload, - {{- else }} - RawPayload: req, - {{- end }} - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.{{ .Name }}(ctx, info, next) - } -} - -{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/example_client_interceptor.go.tpl b/codegen/service/templates/example_client_interceptor.go.tpl new file mode 100644 index 0000000000..455c74c9f4 --- /dev/null +++ b/codegen/service/templates/example_client_interceptor.go.tpl @@ -0,0 +1,24 @@ +// {{ .StructName }}ClientInterceptors implements the client interceptors for the {{ .ServiceName }} service. +type {{ .StructName }}ClientInterceptors struct { +} + +// New{{ .StructName }}ClientInterceptors creates a new client interceptor for the {{ .ServiceName }} service. +func New{{ .StructName }}ClientInterceptors() *{{ .StructName }}ClientInterceptors { + return &{{ .StructName }}ClientInterceptors{} +} + +{{- range .ClientInterceptors }} +{{- if .Description }} +{{ comment .Description }} +{{- end }} +func (i *{{ $.StructName }}ClientInterceptors) {{ .Name }}(ctx context.Context, info *{{ $.PkgName }}.{{ .Name }}Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[{{ .Name }}] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[{{ .Name }}] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[{{ .Name }}] Received response: %v", resp) + return resp, nil +} +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/security_authfuncs.go.tpl b/codegen/service/templates/example_security_authfuncs.go.tpl similarity index 100% rename from codegen/service/templates/security_authfuncs.go.tpl rename to codegen/service/templates/example_security_authfuncs.go.tpl diff --git a/codegen/service/templates/example_server_interceptor.go.tpl b/codegen/service/templates/example_server_interceptor.go.tpl new file mode 100644 index 0000000000..dfbb7d93bb --- /dev/null +++ b/codegen/service/templates/example_server_interceptor.go.tpl @@ -0,0 +1,24 @@ +// {{ .StructName }}ServerInterceptors implements the server interceptor for the {{ .ServiceName }} service. +type {{ .StructName }}ServerInterceptors struct { +} + +// New{{ .StructName }}ServerInterceptors creates a new server interceptor for the {{ .ServiceName }} service. +func New{{ .StructName }}ServerInterceptors() *{{ .StructName }}ServerInterceptors { + return &{{ .StructName }}ServerInterceptors{} +} + +{{- range .ServerInterceptors }} +{{- if .Description }} +{{ comment .Description }} +{{- end }} +func (i *{{ $.StructName }}ServerInterceptors) {{ .Name }}(ctx context.Context, info *{{ $.PkgName }}.{{ .Name }}Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[{{ .Name }}] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[{{ .Name }}] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[{{ .Name }}] Response: %v", resp) + return resp, nil +} +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/service_init.go.tpl b/codegen/service/templates/example_service_init.go.tpl similarity index 100% rename from codegen/service/templates/service_init.go.tpl rename to codegen/service/templates/example_service_init.go.tpl diff --git a/codegen/service/templates/service_struct.go.tpl b/codegen/service/templates/example_service_struct.go.tpl similarity index 100% rename from codegen/service/templates/service_struct.go.tpl rename to codegen/service/templates/example_service_struct.go.tpl diff --git a/codegen/service/templates/interceptors.go.tpl b/codegen/service/templates/interceptors.go.tpl index 46dfceedb0..5f2024cf33 100644 --- a/codegen/service/templates/interceptors.go.tpl +++ b/codegen/service/templates/interceptors.go.tpl @@ -1,32 +1,7 @@ -{{- if .ServerInterceptors -}} -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { -{{- range .ServerInterceptors }} - {{ comment .Description }} - {{ .Name }}(context.Context, *{{ .Name }}Info, goa.NextFunc) (any, error) -{{- end }} -} -{{- end }} - -{{- if .ClientInterceptors -}} -// ClientInterceptors defines the interface for all client-side interceptors. -// Client interceptors execute after the payload is encoded and before the request -// is sent to the server (request interceptors) or after the response is decoded -// and before the result is returned to the client (response interceptors). -type ClientInterceptors interface { -{{- range .ClientInterceptors }} - {{ comment .Description }} - {{ .Name }}(context.Context, *{{ .Name }}Info, goa.NextFunc) (any, error) -{{- end }} -} -{{- end }} // Access interfaces for interceptor payloads and results type ( -{{- range .AllInterceptors }} +{{- range . }} // {{ .Name }}Info provides metadata about the current interception. // It includes service name, method name, and access to the endpoint. {{ .Name }}Info goa.InterceptorInfo @@ -61,11 +36,11 @@ type ( {{- end }} ) -{{- if .HasPrivateImplementationTypes }} +{{- if hasPrivateImplementationTypes . }} // Private implementation types type ( - {{- range .AllInterceptors }} + {{- range . }} {{- if or .ReadPayload .WritePayload }} {{ .UnexportedName }}PayloadAccess struct { payload {{ .PayloadRef }} @@ -81,7 +56,7 @@ type ( ) // Public accessor methods for Info types -{{- range .AllInterceptors }} +{{- range . }} {{- if or .ReadPayload .WritePayload }} // Payload returns a type-safe accessor for the method payload. func (info *{{ .Name }}Info) Payload() {{ .Name }}PayloadAccess { @@ -98,7 +73,7 @@ func (info *{{ .Name }}Info) Result(res any) {{ .Name }}ResultAccess { {{- end }} // Private implementation methods -{{- range .AllInterceptors }} +{{- range . }} {{- $interceptor := . }} {{- range .ReadPayload }} func (p *{{ $interceptor.UnexportedName }}PayloadAccess) {{ .Name }}() {{ .TypeRef }} { diff --git a/codegen/service/templates/server_interceptor_wrappers.go.tpl b/codegen/service/templates/server_interceptor_wrappers.go.tpl new file mode 100644 index 0000000000..4971d6510e --- /dev/null +++ b/codegen/service/templates/server_interceptor_wrappers.go.tpl @@ -0,0 +1,18 @@ +{{- range .ServerInterceptors }} +{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .DesignName) }} +func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &{{ .Name }}Info{ + Service: "{{ $.Service }}", + Method: method, + Endpoint: endpoint, + {{- if .ServerStreamInputStruct }} + RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + return i.{{ .Name }}(ctx, info, endpoint) + } +} +{{- end }} diff --git a/codegen/service/templates/server_interceptors.go.tpl b/codegen/service/templates/server_interceptors.go.tpl new file mode 100644 index 0000000000..c19597a383 --- /dev/null +++ b/codegen/service/templates/server_interceptors.go.tpl @@ -0,0 +1,12 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { +{{- range .ServerInterceptors }} +{{- if .Description }} + {{ comment .Description }} +{{- end }} + {{ .Name }}(ctx context.Context, info *{{ .Name }}Info, next goa.Endpoint) (any, error) +{{- end }} +} \ No newline at end of file diff --git a/codegen/service/testdata/client_code.go b/codegen/service/testdata/client_code.go index 8153742480..a13cacde3a 100644 --- a/codegen/service/testdata/client_code.go +++ b/codegen/service/testdata/client_code.go @@ -307,26 +307,4 @@ func (c *Client) Method(ctx context.Context, p string) (res string, err error) { } return ires.(string), nil } - -// WrapMethodClientEndpoint wraps the Method endpoint with the client -// interceptors defined in the design. -func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientTracing(endpoint, i, "Method") - return endpoint -} - -// wrapClientTracing applies the tracing interceptor to endpoints. -func wrapClientTracing(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &TracingInfo{ - Service: "ServiceWithClientInterceptor", - Method: method, - Endpoint: endpoint, - RawPayload: req, - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.Tracing(ctx, info, next) - } -}` +` diff --git a/codegen/service/testdata/endpoint_code.go b/codegen/service/testdata/endpoint_code.go index 98db14f0d1..324d104ae8 100644 --- a/codegen/service/testdata/endpoint_code.go +++ b/codegen/service/testdata/endpoint_code.go @@ -543,29 +543,7 @@ func NewMethodEndpoint(s Service) goa.Endpoint { return s.Method(ctx, p) } } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") - return endpoint -} - -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &LoggingInfo{ - Service: "ServiceWithServerInterceptor", - Method: method, - Endpoint: endpoint, - RawPayload: req, - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.Logging(ctx, info, next) - } -}` +` var EndpointWithMultipleInterceptors = `// Endpoints wraps the "ServiceWithMultipleInterceptors" service endpoints. type Endpoints struct { @@ -596,46 +574,7 @@ func NewMethodEndpoint(s Service) goa.Endpoint { return s.Method(ctx, p) } } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") - endpoint = wrapMetrics(endpoint, i, "Method") - return endpoint -} - -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &LoggingInfo{ - Service: "ServiceWithMultipleInterceptors", - Method: method, - Endpoint: endpoint, - RawPayload: req, - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.Logging(ctx, info, next) - } -} - -// wrapMetrics applies the metrics interceptor to endpoints. -func wrapMetrics(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &MetricsInfo{ - Service: "ServiceWithMultipleInterceptors", - Method: method, - Endpoint: endpoint, - RawPayload: req, - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.Metrics(ctx, info, next) - } -}` +` var EndpointStreamingWithInterceptor = `// Endpoints wraps the "ServiceStreamingWithInterceptor" service endpoints. type Endpoints struct { @@ -671,26 +610,4 @@ func NewMethodEndpoint(s Service) goa.Endpoint { return nil, s.Method(ctx, ep.Stream) } } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") - return endpoint -} - -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { - return func(ctx context.Context, req any) (any, error) { - info := &LoggingInfo{ - Service: "ServiceStreamingWithInterceptor", - Method: method, - Endpoint: endpoint, - RawPayload: req.(*MethodEndpointInput).Payload, - } - next := func(ctx context.Context) (any, error) { - return endpoint(ctx, req) - } - return i.Logging(ctx, info, next) - } -}` +` diff --git a/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden new file mode 100644 index 0000000000..5188cca366 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden @@ -0,0 +1,35 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// APIInterceptorService example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + apiinterceptorservice "api_interceptor_service" + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" +) + +// APIInterceptorServiceClientInterceptors implements the client interceptors for the APIInterceptorService service. +type APIInterceptorServiceClientInterceptors struct { +} + +// NewAPIInterceptorServiceClientInterceptors creates a new client interceptor for the APIInterceptorService service. +func NewAPIInterceptorServiceClientInterceptors() *APIInterceptorServiceClientInterceptors { + return &APIInterceptorServiceClientInterceptors{} +} +func (i *APIInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden new file mode 100644 index 0000000000..ff86a4cee7 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden @@ -0,0 +1,35 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// APIInterceptorService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + apiinterceptorservice "api_interceptor_service" + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" +) + +// APIInterceptorServiceServerInterceptors implements the server interceptor for the APIInterceptorService service. +type APIInterceptorServiceServerInterceptors struct { +} + +// NewAPIInterceptorServiceServerInterceptors creates a new server interceptor for the APIInterceptorService service. +func NewAPIInterceptorServiceServerInterceptors() *APIInterceptorServiceServerInterceptors { + return &APIInterceptorServiceServerInterceptors{} +} +func (i *APIInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden new file mode 100644 index 0000000000..bb1b3811fb --- /dev/null +++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden @@ -0,0 +1,55 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// ChainedInterceptorService example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + chainedinterceptorservice "chained_interceptor_service" + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" +) + +// ChainedInterceptorServiceClientInterceptors implements the client interceptors for the ChainedInterceptorService service. +type ChainedInterceptorServiceClientInterceptors struct { +} + +// NewChainedInterceptorServiceClientInterceptors creates a new client interceptor for the ChainedInterceptorService service. +func NewChainedInterceptorServiceClientInterceptors() *ChainedInterceptorServiceClientInterceptors { + return &ChainedInterceptorServiceClientInterceptors{} +} +func (i *ChainedInterceptorServiceClientInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Method] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Method] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Method] Received response: %v", resp) + return resp, nil +} +func (i *ChainedInterceptorServiceClientInterceptors) Service(ctx context.Context, info *interceptors.ServiceInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Service] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Service] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Service] Received response: %v", resp) + return resp, nil +} +func (i *ChainedInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden new file mode 100644 index 0000000000..b6cb4245f7 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden @@ -0,0 +1,55 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// ChainedInterceptorService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + chainedinterceptorservice "chained_interceptor_service" + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" +) + +// ChainedInterceptorServiceServerInterceptors implements the server interceptor for the ChainedInterceptorService service. +type ChainedInterceptorServiceServerInterceptors struct { +} + +// NewChainedInterceptorServiceServerInterceptors creates a new server interceptor for the ChainedInterceptorService service. +func NewChainedInterceptorServiceServerInterceptors() *ChainedInterceptorServiceServerInterceptors { + return &ChainedInterceptorServiceServerInterceptors{} +} +func (i *ChainedInterceptorServiceServerInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Method] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Method] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Method] Response: %v", resp) + return resp, nil +} +func (i *ChainedInterceptorServiceServerInterceptors) Service(ctx context.Context, info *interceptors.ServiceInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Service] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Service] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Service] Response: %v", resp) + return resp, nil +} +func (i *ChainedInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden new file mode 100644 index 0000000000..f64ea4bd10 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden @@ -0,0 +1,35 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// ClientInterceptorService example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + clientinterceptorservice "client_interceptor_service" + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" +) + +// ClientInterceptorServiceClientInterceptors implements the client interceptors for the ClientInterceptorService service. +type ClientInterceptorServiceClientInterceptors struct { +} + +// NewClientInterceptorServiceClientInterceptors creates a new client interceptor for the ClientInterceptorService service. +func NewClientInterceptorServiceClientInterceptors() *ClientInterceptorServiceClientInterceptors { + return &ClientInterceptorServiceClientInterceptors{} +} +func (i *ClientInterceptorServiceClientInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden new file mode 100644 index 0000000000..3086ebce99 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleInterceptorsService example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleinterceptorsservice "multiple_interceptors_service" +) + +// MultipleInterceptorsServiceClientInterceptors implements the client interceptors for the MultipleInterceptorsService service. +type MultipleInterceptorsServiceClientInterceptors struct { +} + +// NewMultipleInterceptorsServiceClientInterceptors creates a new client interceptor for the MultipleInterceptorsService service. +func NewMultipleInterceptorsServiceClientInterceptors() *MultipleInterceptorsServiceClientInterceptors { + return &MultipleInterceptorsServiceClientInterceptors{} +} +func (i *MultipleInterceptorsServiceClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test2] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test2] Received response: %v", resp) + return resp, nil +} +func (i *MultipleInterceptorsServiceClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test4] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test4] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden new file mode 100644 index 0000000000..75f774ef53 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleInterceptorsService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleinterceptorsservice "multiple_interceptors_service" +) + +// MultipleInterceptorsServiceServerInterceptors implements the server interceptor for the MultipleInterceptorsService service. +type MultipleInterceptorsServiceServerInterceptors struct { +} + +// NewMultipleInterceptorsServiceServerInterceptors creates a new server interceptor for the MultipleInterceptorsService service. +func NewMultipleInterceptorsServiceServerInterceptors() *MultipleInterceptorsServiceServerInterceptors { + return &MultipleInterceptorsServiceServerInterceptors{} +} +func (i *MultipleInterceptorsServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Response: %v", resp) + return resp, nil +} +func (i *MultipleInterceptorsServiceServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test3] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test3] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden new file mode 100644 index 0000000000..ff9d84033d --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleServicesInterceptorsService2 example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleservicesinterceptorsservice2 "multiple_services_interceptors_service2" +) + +// MultipleServicesInterceptorsService2ClientInterceptors implements the client interceptors for the MultipleServicesInterceptorsService2 service. +type MultipleServicesInterceptorsService2ClientInterceptors struct { +} + +// NewMultipleServicesInterceptorsService2ClientInterceptors creates a new client interceptor for the MultipleServicesInterceptorsService2 service. +func NewMultipleServicesInterceptorsService2ClientInterceptors() *MultipleServicesInterceptorsService2ClientInterceptors { + return &MultipleServicesInterceptorsService2ClientInterceptors{} +} +func (i *MultipleServicesInterceptorsService2ClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test2] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test2] Received response: %v", resp) + return resp, nil +} +func (i *MultipleServicesInterceptorsService2ClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test4] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test4] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden new file mode 100644 index 0000000000..173144f090 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleServicesInterceptorsService2 example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleservicesinterceptorsservice2 "multiple_services_interceptors_service2" +) + +// MultipleServicesInterceptorsService2ServerInterceptors implements the server interceptor for the MultipleServicesInterceptorsService2 service. +type MultipleServicesInterceptorsService2ServerInterceptors struct { +} + +// NewMultipleServicesInterceptorsService2ServerInterceptors creates a new server interceptor for the MultipleServicesInterceptorsService2 service. +func NewMultipleServicesInterceptorsService2ServerInterceptors() *MultipleServicesInterceptorsService2ServerInterceptors { + return &MultipleServicesInterceptorsService2ServerInterceptors{} +} +func (i *MultipleServicesInterceptorsService2ServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Response: %v", resp) + return resp, nil +} +func (i *MultipleServicesInterceptorsService2ServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test3] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test3] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden new file mode 100644 index 0000000000..72c48c6bc8 --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleServicesInterceptorsService example client interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleservicesinterceptorsservice "multiple_services_interceptors_service" +) + +// MultipleServicesInterceptorsServiceClientInterceptors implements the client interceptors for the MultipleServicesInterceptorsService service. +type MultipleServicesInterceptorsServiceClientInterceptors struct { +} + +// NewMultipleServicesInterceptorsServiceClientInterceptors creates a new client interceptor for the MultipleServicesInterceptorsService service. +func NewMultipleServicesInterceptorsServiceClientInterceptors() *MultipleServicesInterceptorsServiceClientInterceptors { + return &MultipleServicesInterceptorsServiceClientInterceptors{} +} +func (i *MultipleServicesInterceptorsServiceClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test2] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test2] Received response: %v", resp) + return resp, nil +} +func (i *MultipleServicesInterceptorsServiceClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test4] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test4] Received response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden new file mode 100644 index 0000000000..4e780407ea --- /dev/null +++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden @@ -0,0 +1,45 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// MultipleServicesInterceptorsService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + multipleservicesinterceptorsservice "multiple_services_interceptors_service" +) + +// MultipleServicesInterceptorsServiceServerInterceptors implements the server interceptor for the MultipleServicesInterceptorsService service. +type MultipleServicesInterceptorsServiceServerInterceptors struct { +} + +// NewMultipleServicesInterceptorsServiceServerInterceptors creates a new server interceptor for the MultipleServicesInterceptorsService service. +func NewMultipleServicesInterceptorsServiceServerInterceptors() *MultipleServicesInterceptorsServiceServerInterceptors { + return &MultipleServicesInterceptorsServiceServerInterceptors{} +} +func (i *MultipleServicesInterceptorsServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Response: %v", resp) + return resp, nil +} +func (i *MultipleServicesInterceptorsServiceServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test3] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test3] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden b/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden new file mode 100644 index 0000000000..2660f90abe --- /dev/null +++ b/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden @@ -0,0 +1,35 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// ServerInterceptorByNameService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + serverinterceptorbynameservice "server_interceptor_by_name_service" +) + +// ServerInterceptorByNameServiceServerInterceptors implements the server interceptor for the ServerInterceptorByNameService service. +type ServerInterceptorByNameServiceServerInterceptors struct { +} + +// NewServerInterceptorByNameServiceServerInterceptors creates a new server interceptor for the ServerInterceptorByNameService service. +func NewServerInterceptorByNameServiceServerInterceptors() *ServerInterceptorByNameServiceServerInterceptors { + return &ServerInterceptorByNameServiceServerInterceptors{} +} +func (i *ServerInterceptorByNameServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden new file mode 100644 index 0000000000..517fdbc27e --- /dev/null +++ b/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden @@ -0,0 +1,35 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// ServerInterceptorService example server interceptors +// +// Command: +// goa + +package interceptors + +import ( + "context" + "fmt" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + serverinterceptorservice "server_interceptor_service" +) + +// ServerInterceptorServiceServerInterceptors implements the server interceptor for the ServerInterceptorService service. +type ServerInterceptorServiceServerInterceptors struct { +} + +// NewServerInterceptorServiceServerInterceptors creates a new server interceptor for the ServerInterceptorService service. +func NewServerInterceptorServiceServerInterceptors() *ServerInterceptorServiceServerInterceptors { + return &ServerInterceptorServiceServerInterceptors{} +} +func (i *ServerInterceptorServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[Test] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[Test] Response: %v", resp) + return resp, nil +} diff --git a/codegen/service/testdata/example_interceptors_dsls.go b/codegen/service/testdata/example_interceptors_dsls.go new file mode 100644 index 0000000000..42ecbb21ef --- /dev/null +++ b/codegen/service/testdata/example_interceptors_dsls.go @@ -0,0 +1,96 @@ +package testdata + +import ( + . "goa.design/goa/v3/dsl" +) + +var NoInterceptorExampleDSL = func() { + var _ = Service("NoInterceptorService", func() { + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var ServerInterceptorExampleDSL = func() { + var SInterceptor = Interceptor("test") + var _ = Service("ServerInterceptorService", func() { + ServerInterceptor(SInterceptor) + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var ClientInterceptorExampleDSL = func() { + var CInterceptor = Interceptor("test") + var _ = Service("ClientInterceptorService", func() { + ClientInterceptor(CInterceptor) + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var ServerInterceptorByNameExampleDSL = func() { + var _ = Interceptor("test") + var _ = Service("ServerInterceptorByNameService", func() { + ServerInterceptor("test") + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var MultipleInterceptorsExampleDSL = func() { + var _ = Interceptor("test") + var _ = Interceptor("test2") + var SInterceptor = Interceptor("test3") + var CInterceptor = Interceptor("test4") + var _ = Service("MultipleInterceptorsService", func() { + ServerInterceptor("test", SInterceptor) + ClientInterceptor("test2", CInterceptor) + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var MultipleServicesInterceptorsExampleDSL = func() { + var _ = Interceptor("test") + var _ = Interceptor("test2") + var SInterceptor = Interceptor("test3") + var CInterceptor = Interceptor("test4") + var _ = Service("MultipleServicesInterceptorsService", func() { + ServerInterceptor("test", SInterceptor) + ClientInterceptor("test2", CInterceptor) + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) + var _ = Service("MultipleServicesInterceptorsService2", func() { + ServerInterceptor("test", SInterceptor) + ClientInterceptor("test2", CInterceptor) + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var APIInterceptorExampleDSL = func() { + var APIInterceptor = Interceptor("api") + var _ = API("test", func() { + ServerInterceptor(APIInterceptor) + ClientInterceptor(APIInterceptor) + }) + var _ = Service("APIInterceptorService", func() { + Method("Method", func() { HTTP(func() { GET("/") }) }) + }) +} + +var ChainedInterceptorExampleDSL = func() { + var APIInterceptor = Interceptor("api") + var ServiceInterceptor = Interceptor("service") + var MethodInterceptor = Interceptor("method") + + var _ = API("test", func() { + ServerInterceptor(APIInterceptor) + ClientInterceptor(APIInterceptor) + }) + + var _ = Service("ChainedInterceptorService", func() { + ServerInterceptor(ServiceInterceptor) + ClientInterceptor(ServiceInterceptor) + Method("Method", func() { + ServerInterceptor(MethodInterceptor) + ClientInterceptor(MethodInterceptor) + HTTP(func() { GET("/") }) + }) + }) +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden new file mode 100644 index 0000000000..f1a4d2805c --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..ffbd9bf910 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapValidation applies the validation interceptor to endpoints. +func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithReadPayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} + +// wrapClientValidation applies the validation interceptor to endpoints. +func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithReadPayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden similarity index 70% rename from codegen/service/testdata/interceptors/interceptor-with-read-payload.golden rename to codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden index eb5152b895..e3edffbad9 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-payload.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -38,4 +38,9 @@ func (p *validationPayloadAccess) Name() string { return p.payload.Name } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden new file mode 100644 index 0000000000..76648aff03 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..c4ad755b66 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapCaching applies the caching interceptor to endpoints. +func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithReadResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} + +// wrapClientCaching applies the caching interceptor to endpoints. +func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithReadResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden similarity index 69% rename from codegen/service/testdata/interceptors/interceptor-with-read-result.golden rename to codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden index 254d1ddaf0..de70b2f1b3 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-result.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -38,4 +38,9 @@ func (r *cachingResultAccess) Data() string { return r.result.Data } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden new file mode 100644 index 0000000000..f1a4d2805c --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..c9947a357a --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapValidation applies the validation interceptor to endpoints. +func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithReadWritePayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} + +// wrapClientValidation applies the validation interceptor to endpoints. +func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithReadWritePayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden similarity index 71% rename from codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden rename to codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden index 07d0f938cb..f4242b3d97 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -42,4 +42,9 @@ func (p *validationPayloadAccess) SetName(v string) { p.payload.Name = v } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden new file mode 100644 index 0000000000..76648aff03 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..65bbf27b56 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapCaching applies the caching interceptor to endpoints. +func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithReadWriteResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} + +// wrapClientCaching applies the caching interceptor to endpoints. +func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithReadWriteResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden similarity index 71% rename from codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden rename to codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden index 202260177d..ef8f84f67f 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-result.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -42,4 +42,9 @@ func (r *cachingResultAccess) SetData(v string) { r.result.Data = v } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden new file mode 100644 index 0000000000..f1a4d2805c --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..34e9a283bd --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapValidation applies the validation interceptor to endpoints. +func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithWritePayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} + +// wrapClientValidation applies the validation interceptor to endpoints. +func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &ValidationInfo{ + Service: "InterceptorWithWritePayload", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden similarity index 70% rename from codegen/service/testdata/interceptors/interceptor-with-write-payload.golden rename to codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden index 7f0eabeb04..e54173d388 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-payload.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Validation(context.Context, *ValidationInfo, goa.NextFunc) (any, error) + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -38,4 +38,9 @@ func (p *validationPayloadAccess) SetName(v string) { p.payload.Name = v } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapValidation(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden new file mode 100644 index 0000000000..76648aff03 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden @@ -0,0 +1,14 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) +} + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..fad8d4bbe7 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden @@ -0,0 +1,26 @@ + +// wrapCaching applies the caching interceptor to endpoints. +func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithWriteResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} + +// wrapClientCaching applies the caching interceptor to endpoints. +func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &CachingInfo{ + Service: "InterceptorWithWriteResult", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden similarity index 69% rename from codegen/service/testdata/interceptors/interceptor-with-write-result.golden rename to codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden index 362ae77d6a..1efc553c96 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-result.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Caching(context.Context, *CachingInfo, goa.NextFunc) (any, error) + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -38,4 +38,9 @@ func (r *cachingResultAccess) SetData(v string) { r.result.Data = v } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapCaching(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/multiple-interceptors.golden b/codegen/service/testdata/interceptors/multiple-interceptors.golden deleted file mode 100644 index 111d148050..0000000000 --- a/codegen/service/testdata/interceptors/multiple-interceptors.golden +++ /dev/null @@ -1,30 +0,0 @@ -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) - - Tracing(context.Context, *TracingInfo, goa.NextFunc) (any, error) -} // ClientInterceptors defines the interface for all client-side interceptors. -// Client interceptors execute after the payload is encoded and before the request -// is sent to the server (request interceptors) or after the response is decoded -// and before the result is returned to the client (response interceptors). -type ClientInterceptors interface { - Metrics(context.Context, *MetricsInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // LoggingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - LoggingInfo goa.InterceptorInfo - // TracingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - TracingInfo goa.InterceptorInfo - // MetricsInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - MetricsInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden new file mode 100644 index 0000000000..b0f78bbbb5 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden @@ -0,0 +1,26 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Test2(ctx context.Context, info *Test2Info, next goa.Endpoint) (any, error) + Test4(ctx context.Context, info *Test4Info, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // Test2Info provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + Test2Info goa.InterceptorInfo + // Test4Info provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + Test4Info goa.InterceptorInfo +) + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientTest2(endpoint, i, "Method") + endpoint = wrapClientTest4(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..dd2afccd42 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden @@ -0,0 +1,52 @@ + +// wrapTest applies the test interceptor to endpoints. +func wrapTest(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &TestInfo{ + Service: "MultipleInterceptorsService", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Test(ctx, info, endpoint) + } +} + +// wrapTest3 applies the test3 interceptor to endpoints. +func wrapTest3(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &Test3Info{ + Service: "MultipleInterceptorsService", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Test3(ctx, info, endpoint) + } +} + +// wrapClientTest2 applies the test2 interceptor to endpoints. +func wrapClientTest2(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &Test2Info{ + Service: "MultipleInterceptorsService", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Test2(ctx, info, endpoint) + } +} + +// wrapClientTest4 applies the test4 interceptor to endpoints. +func wrapClientTest4(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &Test4Info{ + Service: "MultipleInterceptorsService", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Test4(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden new file mode 100644 index 0000000000..7d8c68e506 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden @@ -0,0 +1,26 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { + Test(ctx context.Context, info *TestInfo, next goa.Endpoint) (any, error) + Test3(ctx context.Context, info *Test3Info, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // TestInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + TestInfo goa.InterceptorInfo + // Test3Info provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + Test3Info goa.InterceptorInfo +) + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapTest(endpoint, i, "Method") + endpoint = wrapTest3(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor.golden deleted file mode 100644 index ae003b0548..0000000000 --- a/codegen/service/testdata/interceptors/single-api-server-interceptor.golden +++ /dev/null @@ -1,16 +0,0 @@ -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // LoggingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - LoggingInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..1decfd7599 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "SingleAPIServerInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden new file mode 100644 index 0000000000..a5af7d39d5 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,28 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} + +// WrapMethod2Endpoint wraps the Method2 endpoint with the server-side +// interceptors defined in the design. +func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method2") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/single-client-interceptor.golden b/codegen/service/testdata/interceptors/single-client-interceptor.golden deleted file mode 100644 index 0fc4e0f1a9..0000000000 --- a/codegen/service/testdata/interceptors/single-client-interceptor.golden +++ /dev/null @@ -1,16 +0,0 @@ -// ClientInterceptors defines the interface for all client-side interceptors. -// Client interceptors execute after the payload is encoded and before the request -// is sent to the server (request interceptors) or after the response is decoded -// and before the result is returned to the client (response interceptors). -type ClientInterceptors interface { - Tracing(context.Context, *TracingInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // TracingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - TracingInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden new file mode 100644 index 0000000000..41f9b16414 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden @@ -0,0 +1,21 @@ +// ClientInterceptors defines the interface for all client-side interceptors. +// Client interceptors execute after the payload is encoded and before the request +// is sent to the server. The implementation is responsible for calling next to +// complete the request. +type ClientInterceptors interface { + Tracing(ctx context.Context, info *TracingInfo, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // TracingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + TracingInfo goa.InterceptorInfo +) + +// WrapMethodClientEndpoint wraps the Method endpoint with the client +// interceptors defined in the design. +func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { + endpoint = wrapClientTracing(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..4744840adf --- /dev/null +++ b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapClientTracing applies the tracing interceptor to endpoints. +func wrapClientTracing(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &TracingInfo{ + Service: "SingleClientInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Tracing(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor.golden deleted file mode 100644 index ae003b0548..0000000000 --- a/codegen/service/testdata/interceptors/single-method-server-interceptor.golden +++ /dev/null @@ -1,16 +0,0 @@ -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // LoggingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - LoggingInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..57c9dce068 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "SingleMethodServerInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden new file mode 100644 index 0000000000..05e445a7f2 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,21 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor.golden deleted file mode 100644 index ae003b0548..0000000000 --- a/codegen/service/testdata/interceptors/single-service-server-interceptor.golden +++ /dev/null @@ -1,16 +0,0 @@ -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // LoggingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - LoggingInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..6e8b74a57c --- /dev/null +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "SingleServerInterceptor", + Method: method, + Endpoint: endpoint, + RawPayload: req, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden new file mode 100644 index 0000000000..a5af7d39d5 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,28 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} + +// WrapMethod2Endpoint wraps the Method2 endpoint with the server-side +// interceptors defined in the design. +func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method2") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..f5a3f738df --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "StreamingInterceptorsWithReadPayload", + Method: method, + Endpoint: endpoint, + RawPayload: req.(*MethodEndpointInput).Payload, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden similarity index 71% rename from codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden rename to codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden index 68f1cf20cb..8f6ed7f94c 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -42,4 +42,9 @@ func (p *loggingPayloadAccess) Initial() string { return *p.payload.Initial } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..2cb886bc02 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "StreamingInterceptorsWithReadResult", + Method: method, + Endpoint: endpoint, + RawPayload: req.(*MethodEndpointInput).Payload, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden similarity index 70% rename from codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden rename to codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden index 4dec14cb90..bc5516387c 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden @@ -1,9 +1,9 @@ // ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) } // Access interfaces for interceptor payloads and results @@ -42,4 +42,9 @@ func (r *loggingResultAccess) Data() string { return *r.result.Data } - +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors.golden b/codegen/service/testdata/interceptors/streaming-interceptors.golden deleted file mode 100644 index ae003b0548..0000000000 --- a/codegen/service/testdata/interceptors/streaming-interceptors.golden +++ /dev/null @@ -1,16 +0,0 @@ -// ServerInterceptors defines the interface for all server-side interceptors. -// Server interceptors execute after the request is decoded and before the payload -// is sent to the service (request interceptors) or after the service returns and -// before the response is encoded (response interceptors). -type ServerInterceptors interface { - Logging(context.Context, *LoggingInfo, goa.NextFunc) (any, error) -} - -// Access interfaces for interceptor payloads and results -type ( - // LoggingInfo provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - LoggingInfo goa.InterceptorInfo -) - - diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..e0841bc4be --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden @@ -0,0 +1,13 @@ + +// wrapLogging applies the logging interceptor to endpoints. +func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "StreamingInterceptors", + Method: method, + Endpoint: endpoint, + RawPayload: req.(*MethodEndpointInput).Payload, + } + return i.Logging(ctx, info, endpoint) + } +} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden new file mode 100644 index 0000000000..05e445a7f2 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden @@ -0,0 +1,21 @@ +// ServerInterceptors defines the interface for all server-side interceptors. +// Server interceptors execute after the request is decoded and before the +// payload is sent to the service. The implementation is responsible for calling +// next to complete the request. +type ServerInterceptors interface { + Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error) +} + +// Access interfaces for interceptor payloads and results +type ( + // LoggingInfo provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + LoggingInfo goa.InterceptorInfo +) + +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapLogging(endpoint, i, "Method") + return endpoint +} diff --git a/dsl/method_test.go b/dsl/method_test.go index 30030fdbfd..d450ecf2f0 100644 --- a/dsl/method_test.go +++ b/dsl/method_test.go @@ -45,15 +45,14 @@ func TestMethod(t *testing.T) { t.Fatalf("b: expected 1 method, got %d", len(methods)) } method := methods[0] - doc := method.Docs - if doc == nil { + if method.Docs == nil { t.Fatalf("b: method docs is nil") } - if doc.Description != desc { - t.Errorf("b: expected docs description '%s' to match '%s' ", desc, doc.Description) + if method.Docs.Description != desc { + t.Errorf("b: expected docs description '%s' to match '%s' ", desc, method.Docs.Description) } - if doc.URL != url { - t.Errorf("b: expected docs url '%s' to match '%s' ", url, doc.URL) + if method.Docs.URL != url { + t.Errorf("b: expected docs url '%s' to match '%s' ", url, method.Docs.URL) } }, }, @@ -74,19 +73,20 @@ func TestMethod(t *testing.T) { method := methods[0] if method == nil { t.Fatalf("c: method is nil") + return // Make linter happy } - payload := method.Payload - if payload == nil { + if method.Payload == nil { t.Fatalf("c: method payload is nil") + return // Make linter happy } - if payload.Description != desc { - t.Errorf("c: expected payload description '%s' to match '%s' ", desc, payload.Description) + if method.Payload.Description != desc { + t.Errorf("c: expected payload description '%s' to match '%s' ", desc, method.Payload.Description) } - obj := expr.AsObject(payload.Type) + obj := expr.AsObject(method.Payload.Type) if att := obj.Attribute("required"); att == nil { t.Errorf("c: expected a payload field with key required") } - if !payload.IsRequired("required") { + if !method.Payload.IsRequired("required") { t.Errorf("c: expected the required field to be required") } }, diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go index 605438087c..b1d40f45b5 100644 --- a/expr/http_endpoint_test.go +++ b/expr/http_endpoint_test.go @@ -215,8 +215,8 @@ func TestHTTPEndpointParentRequired(t *testing.T) { t.Fatal(`unexpected error, service "Child" not found`) } m := svc.Method("Method") - if m == nil { - t.Fatal(`unexpected error, method "Method" not found`) + if m == nil || m.Payload == nil { + t.Fatal(`unexpected error, method "Method" or its payload not found`) } if !m.Payload.IsRequired("ancestor_id") { t.Errorf(`expected "ancestor_id" is required, but not so`) diff --git a/expr/method_test.go b/expr/method_test.go index bd2bef8611..d23d58ddc7 100644 --- a/expr/method_test.go +++ b/expr/method_test.go @@ -154,3 +154,103 @@ func TestMethodExprIsPayloadStreaming(t *testing.T) { } } } + +func TestMethodExprPrepare(t *testing.T) { + var ( + apiInterceptor = &expr.InterceptorExpr{Name: "api"} + svcInterceptor = &expr.InterceptorExpr{Name: "service"} + mtdInterceptor = &expr.InterceptorExpr{Name: "method"} + ) + + cases := map[string]struct { + api *expr.APIExpr + service *expr.ServiceExpr + method *expr.MethodExpr + expectedServer []*expr.InterceptorExpr + expectedClient []*expr.InterceptorExpr + }{ + "no-interceptors": { + api: &expr.APIExpr{}, + service: &expr.ServiceExpr{}, + method: &expr.MethodExpr{}, + expectedServer: []*expr.InterceptorExpr{}, + expectedClient: []*expr.InterceptorExpr{}, + }, + "api-only": { + api: &expr.APIExpr{ + ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + }, + service: &expr.ServiceExpr{}, + method: &expr.MethodExpr{}, + expectedServer: []*expr.InterceptorExpr{apiInterceptor}, + expectedClient: []*expr.InterceptorExpr{apiInterceptor}, + }, + "service-only": { + api: &expr.APIExpr{}, + service: &expr.ServiceExpr{ + ServerInterceptors: []*expr.InterceptorExpr{svcInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{svcInterceptor}, + }, + method: &expr.MethodExpr{}, + expectedServer: []*expr.InterceptorExpr{svcInterceptor}, + expectedClient: []*expr.InterceptorExpr{svcInterceptor}, + }, + "method-only": { + api: &expr.APIExpr{}, + service: &expr.ServiceExpr{}, + method: &expr.MethodExpr{ + ServerInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, + }, + expectedServer: []*expr.InterceptorExpr{mtdInterceptor}, + expectedClient: []*expr.InterceptorExpr{mtdInterceptor}, + }, + "chained-interceptors": { + api: &expr.APIExpr{ + ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + }, + service: &expr.ServiceExpr{ + ServerInterceptors: []*expr.InterceptorExpr{svcInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{svcInterceptor}, + }, + method: &expr.MethodExpr{ + ServerInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, + }, + expectedServer: []*expr.InterceptorExpr{mtdInterceptor, svcInterceptor, apiInterceptor}, + expectedClient: []*expr.InterceptorExpr{mtdInterceptor, svcInterceptor, apiInterceptor}, + }, + "duplicate-interceptors": { + api: &expr.APIExpr{ + ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + }, + service: &expr.ServiceExpr{ + ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, // Same as API + ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + }, + method: &expr.MethodExpr{ + ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, // Same as API + ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, + }, + expectedServer: []*expr.InterceptorExpr{apiInterceptor}, // Only one copy + expectedClient: []*expr.InterceptorExpr{apiInterceptor}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + expr.Root.API = tc.api + tc.method.Service = tc.service + tc.method.Prepare() + + assert.Equal(t, tc.expectedServer, tc.method.ServerInterceptors) + assert.Equal(t, tc.expectedClient, tc.method.ClientInterceptors) + assert.NotNil(t, tc.method.Payload) + assert.NotNil(t, tc.method.StreamingPayload) + assert.NotNil(t, tc.method.Result) + }) + } +} diff --git a/grpc/codegen/example_cli.go b/grpc/codegen/example_cli.go index 36eee89f7b..8e346f5c2c 100644 --- a/grpc/codegen/example_cli.go +++ b/grpc/codegen/example_cli.go @@ -24,9 +24,10 @@ func ExampleCLIFiles(genpkg string, root *expr.RootExpr) []*codegen.File { // exampleCLI returns an example client tool HTTP implementation for the given // server expression. -func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codegen.File { +func exampleCLI(genpkg string, _ *expr.RootExpr, svr *expr.ServerExpr) *codegen.File { var ( mainPath string + rootPath string svrdata = example.Servers.Get(svr) ) @@ -35,22 +36,11 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg if _, err := os.Stat(mainPath); !os.IsNotExist(err) { return nil // file already exists, skip it. } - } - - var ( - rootPath string - apiPkg string - - scope = codegen.NewNameScope() - ) - { - // genpkg is created by path.Join so the separator is / regardless of operating system idx := strings.LastIndex(genpkg, string("/")) rootPath = "." if idx > 0 { rootPath = genpkg[:idx] } - apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api") } var ( @@ -68,11 +58,18 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg {Path: "time"}, codegen.GoaImport(""), codegen.GoaNamedImport("grpc", "goagrpc"), - {Path: rootPath, Name: apiPkg}, + {Path: rootPath + "/interceptors"}, {Path: path.Join(genpkg, "grpc", "cli", svrdata.Dir), Name: "cli"}, } } + var svcData []*ServiceData + for _, svc := range svr.Services { + if data := GRPCServices.Get(svc); data != nil { + svcData = append(svcData, data) + } + } + var ( sections []*codegen.SectionTemplate ) @@ -82,7 +79,11 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg { Name: "do-grpc-cli", Source: readTemplate("do_grpc_cli"), - Data: svrdata, + Data: map[string]any{ + "DefaultTransport": svrdata.DefaultTransport(), + "Services": svcData, + "InterceptorsPkg": "interceptors", + }, }, } } diff --git a/grpc/codegen/example_cli_test.go b/grpc/codegen/example_cli_test.go index 9a9b59a787..aeb5fffe35 100644 --- a/grpc/codegen/example_cli_test.go +++ b/grpc/codegen/example_cli_test.go @@ -2,6 +2,7 @@ package codegen import ( "bytes" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -9,6 +10,7 @@ import ( "goa.design/goa/v3/codegen/example" ctestdata "goa.design/goa/v3/codegen/example/testdata" "goa.design/goa/v3/expr" + "goa.design/goa/v3/grpc/codegen/testdata" ) func TestExampleCLIFiles(t *testing.T) { @@ -23,6 +25,7 @@ func TestExampleCLIFiles(t *testing.T) { {"no-server-pkgpath", ctestdata.NoServerDSL, "my/pkg/path"}, {"server-hosting-service-subset-pkgpath", ctestdata.ServerHostingServiceSubsetDSL, "my/pkg/path"}, {"server-hosting-multiple-services-pkgpath", ctestdata.ServerHostingMultipleServicesDSL, "my/pkg/path"}, + {"interceptors", testdata.InterceptorsDSL, ""}, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { @@ -37,7 +40,8 @@ func TestExampleCLIFiles(t *testing.T) { require.NoError(t, s.Write(&buf)) } code := codegen.FormatTestCode(t, buf.String()) - compareOrUpdateGolden(t, code, "client-"+c.Name+".golden") + golden := filepath.Join("testdata", "client-"+c.Name+".golden") + compareOrUpdateGolden(t, code, golden) }) } } diff --git a/grpc/codegen/templates/do_grpc_cli.go.tpl b/grpc/codegen/templates/do_grpc_cli.go.tpl index 8802e20c69..bdc52ca3cb 100644 --- a/grpc/codegen/templates/do_grpc_cli.go.tpl +++ b/grpc/codegen/templates/do_grpc_cli.go.tpl @@ -3,7 +3,19 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) +{{- range .Services }} + {{- if .Service.ClientInterceptors }} + {{ .Service.VarName }}Interceptors := {{ $.InterceptorsPkg }}.New{{ .Service.StructName }}ClientInterceptors() + {{- end }} +{{- end }} + return cli.ParseEndpoint( + conn, +{{- range .Services }} + {{- if .Service.ClientInterceptors }} + {{ .Service.VarName }}Interceptors, + {{- end }} +{{- end }} + ) } {{ if eq .DefaultTransport.Type "grpc" }} diff --git a/grpc/codegen/templates/parse_endpoint.go.tpl b/grpc/codegen/templates/parse_endpoint.go.tpl index b7f3eed3ad..3cf69cde5a 100644 --- a/grpc/codegen/templates/parse_endpoint.go.tpl +++ b/grpc/codegen/templates/parse_endpoint.go.tpl @@ -1,6 +1,12 @@ // ParseEndpoint returns the endpoint and payload as specified on the command // line. -func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, any, error) { +func ParseEndpoint( + cc *grpc.ClientConn, + {{- if .Interceptors }} + {{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors, + {{- end }} + opts ...grpc.CallOption, +) (goa.Endpoint, any, error) { {{ .FlagsCode }} var ( data any @@ -13,9 +19,14 @@ func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, case "{{ .Name }}": c := {{ .PkgName }}.NewClient(cc, opts...) switch epn { - {{- $pkgName := .PkgName }}{{ range .Subcommands }} + {{- $pkgName := .PkgName }} + {{- $interceptors := .Interceptors }} + {{ range .Subcommands }} case "{{ .Name }}": endpoint = c.{{ .MethodVarName }}() + {{- if $interceptors }} + endpoint = {{ $interceptors.PkgName }}.Wrap{{ .MethodVarName }}ClientEndpoint(endpoint, {{ $interceptors.VarName }}) + {{- end }} {{- if .BuildFunction }} data, err = {{ $pkgName}}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }}) {{- else if .Conversion }} diff --git a/grpc/codegen/testdata/client-interceptors.golden b/grpc/codegen/testdata/client-interceptors.golden new file mode 100644 index 0000000000..f21f374e1e --- /dev/null +++ b/grpc/codegen/testdata/client-interceptors.golden @@ -0,0 +1,30 @@ +import ( + "fmt" + cli "grpc/cli/test_api" + "os" + + "./interceptors" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { + conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) + } + serviceWithInterceptorsInterceptors := interceptors.NewServiceWithInterceptorsClientInterceptors() + return cli.ParseEndpoint( + conn, + serviceWithInterceptorsInterceptors, + ) +} + +func grpcUsageCommands() string { + return cli.UsageCommands() +} + +func grpcUsageExamples() string { + return cli.UsageExamples() +} diff --git a/grpc/codegen/client-no-server-pkgpath.golden b/grpc/codegen/testdata/client-no-server-pkgpath.golden similarity index 91% rename from grpc/codegen/client-no-server-pkgpath.golden rename to grpc/codegen/testdata/client-no-server-pkgpath.golden index 56ca0253c8..d1033172ff 100644 --- a/grpc/codegen/client-no-server-pkgpath.golden +++ b/grpc/codegen/testdata/client-no-server-pkgpath.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/client-no-server.golden b/grpc/codegen/testdata/client-no-server.golden similarity index 91% rename from grpc/codegen/client-no-server.golden rename to grpc/codegen/testdata/client-no-server.golden index b6a7c341da..e9259418ae 100644 --- a/grpc/codegen/client-no-server.golden +++ b/grpc/codegen/testdata/client-no-server.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden b/grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden similarity index 92% rename from grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden rename to grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden index 22ea9249b5..5324d085af 100644 --- a/grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden +++ b/grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/client-server-hosting-multiple-services.golden b/grpc/codegen/testdata/client-server-hosting-multiple-services.golden similarity index 91% rename from grpc/codegen/client-server-hosting-multiple-services.golden rename to grpc/codegen/testdata/client-server-hosting-multiple-services.golden index 6956659be3..dcfe3180eb 100644 --- a/grpc/codegen/client-server-hosting-multiple-services.golden +++ b/grpc/codegen/testdata/client-server-hosting-multiple-services.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/client-server-hosting-service-subset-pkgpath.golden b/grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden similarity index 92% rename from grpc/codegen/client-server-hosting-service-subset-pkgpath.golden rename to grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden index 22ea9249b5..5324d085af 100644 --- a/grpc/codegen/client-server-hosting-service-subset-pkgpath.golden +++ b/grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/client-server-hosting-service-subset.golden b/grpc/codegen/testdata/client-server-hosting-service-subset.golden similarity index 91% rename from grpc/codegen/client-server-hosting-service-subset.golden rename to grpc/codegen/testdata/client-server-hosting-service-subset.golden index 6956659be3..dcfe3180eb 100644 --- a/grpc/codegen/client-server-hosting-service-subset.golden +++ b/grpc/codegen/testdata/client-server-hosting-service-subset.golden @@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { if err != nil { fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) } - return cli.ParseEndpoint(conn) + return cli.ParseEndpoint( + conn, + ) } diff --git a/grpc/codegen/testdata/dsls.go b/grpc/codegen/testdata/dsls.go index f7c7c5c7ca..743ce71cb8 100644 --- a/grpc/codegen/testdata/dsls.go +++ b/grpc/codegen/testdata/dsls.go @@ -1009,3 +1009,27 @@ var CustomMessageNameDSL = func() { }) }) } + +var InterceptorsDSL = func() { + var LogInterceptor = Interceptor("Log", func() { + Description("Logs request and response details") + }) + var MetricsInterceptor = Interceptor("Metrics", func() { + Description("Collects metrics for the operation") + }) + + Service("ServiceWithInterceptors", func() { + ClientInterceptor(LogInterceptor) + Method("MethodA", func() { + ClientInterceptor(MetricsInterceptor) + Payload(String) + Result(String) + GRPC(func() {}) + }) + Method("MethodB", func() { + Payload(Int) + Result(Int) + GRPC(func() {}) + }) + }) +} diff --git a/http/codegen/client_cli.go b/http/codegen/client_cli.go index 73a787e54a..9cf67f2888 100644 --- a/http/codegen/client_cli.go +++ b/http/codegen/client_cli.go @@ -127,6 +127,13 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da Path: genpkg + "/http/" + sd.Service.PathName + "/client", Name: sd.Service.PkgName + "c", }) + // Add interceptors import if service has client interceptors + if len(sd.Service.ClientInterceptors) > 0 { + specs = append(specs, &codegen.ImportSpec{ + Path: genpkg + "/" + sd.Service.PathName, + Name: sd.Service.PkgName, + }) + } } cliData := make([]*cli.CommandData, len(data)) diff --git a/http/codegen/example_cli.go b/http/codegen/example_cli.go index fe7b6e4968..c6d57c8340 100644 --- a/http/codegen/example_cli.go +++ b/http/codegen/example_cli.go @@ -7,6 +7,7 @@ import ( "goa.design/goa/v3/codegen" "goa.design/goa/v3/codegen/example" + "goa.design/goa/v3/codegen/service" "goa.design/goa/v3/expr" ) @@ -30,20 +31,10 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg if _, err := os.Stat(path); !os.IsNotExist(err) { return nil // file already exists, skip it. } - var ( - rootPath string - apiPkg string - - scope = codegen.NewNameScope() - ) - { - // genpkg is created by path.Join so the separator is / regardless of operating system - idx := strings.LastIndex(genpkg, string("/")) - rootPath = "." - if idx > 0 { - rootPath = genpkg[:idx] - } - apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api") + idx := strings.LastIndex(genpkg, string("/")) + rootPath := "." + if idx > 0 { + rootPath = genpkg[:idx] } specs := []*codegen.ImportSpec{ {Path: "context"}, @@ -59,8 +50,17 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg codegen.GoaImport(""), codegen.GoaNamedImport("http", "goahttp"), {Path: genpkg + "/http/cli/" + svrdata.Dir, Name: "cli"}, - {Path: rootPath, Name: apiPkg}, } + importScope := codegen.NewNameScope() + for _, svc := range root.Services { + data := service.Services.Get(svc.Name) + specs = append(specs, &codegen.ImportSpec{Path: genpkg + "/" + data.PkgName}) + importScope.Unique(data.PkgName) + } + interceptorsPkg := importScope.Unique("interceptors", "ex") + specs = append(specs, &codegen.ImportSpec{Path: rootPath + "/interceptors", Name: interceptorsPkg}) + apiPkg := importScope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api") + specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg}) var svcData []*ServiceData for _, svc := range svr.Services { @@ -73,6 +73,10 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg { Name: "cli-http-start", Source: readTemplate("cli_start"), + Data: map[string]any{ + "Services": svcData, + "InterceptorsPkg": interceptorsPkg, + }, }, { Name: "cli-http-streaming", diff --git a/http/codegen/templates/cli_end.go.tpl b/http/codegen/templates/cli_end.go.tpl index f94e8b7878..31c52c9eb3 100644 --- a/http/codegen/templates/cli_end.go.tpl +++ b/http/codegen/templates/cli_end.go.tpl @@ -5,20 +5,25 @@ return cli.ParseEndpoint( goahttp.RequestEncoder, goahttp.ResponseDecoder, debug, - {{- if needStream .Services }} +{{- if needStream .Services }} dialer, - {{- range $svc := .Services }} - {{- if hasWebSocket $svc }} - nil, - {{- end }} - {{- end }} + {{- range $svc := .Services }} + {{- if hasWebSocket $svc }} + nil, {{- end }} - {{- range .Services }} - {{- range .Endpoints }} - {{- if .MultipartRequestDecoder }} + {{- end }} +{{- end }} +{{- range .Services }} + {{- range .Endpoints }} + {{- if .MultipartRequestDecoder }} {{ $.APIPkg }}.{{ .MultipartRequestEncoder.FuncName }}, - {{- end }} - {{- end }} {{- end }} + {{- end }} +{{- end }} +{{- range .Services }} + {{- if .Service.ClientInterceptors }} + {{ .Service.VarName }}Interceptors, + {{- end }} +{{- end }} ) -} +} \ No newline at end of file diff --git a/http/codegen/templates/cli_start.go.tpl b/http/codegen/templates/cli_start.go.tpl index c068dd6f7a..8ca80e51f8 100644 --- a/http/codegen/templates/cli_start.go.tpl +++ b/http/codegen/templates/cli_start.go.tpl @@ -1,10 +1,20 @@ func doHTTP(scheme, host string, timeout int, debug bool) (goa.Endpoint, any, error) { var ( doer goahttp.Doer +{{- range .Services }} + {{- if .Service.ClientInterceptors }} + {{ .Service.VarName }}Interceptors {{ .Service.PkgName }}.ClientInterceptors + {{- end }} +{{- end }} ) { doer = &http.Client{Timeout: time.Duration(timeout) * time.Second} if debug { doer = goahttp.NewDebugDoer(doer) } +{{- range .Services }} + {{- if .Service.ClientInterceptors }} + {{ .Service.VarName }}Interceptors = {{ $.InterceptorsPkg }}.New{{ .Service.StructName }}ClientInterceptors() + {{- end }} +{{- end }} } diff --git a/http/codegen/templates/parse_endpoint.go.tpl b/http/codegen/templates/parse_endpoint.go.tpl index d967bcfdcb..fc55279552 100644 --- a/http/codegen/templates/parse_endpoint.go.tpl +++ b/http/codegen/templates/parse_endpoint.go.tpl @@ -14,12 +14,15 @@ func ParseEndpoint( {{- end }} {{- end }} {{- end }} - {{- range $c := .Commands }} + {{- range $i, $c := .Commands }} {{- range .Subcommands }} {{- if .MultipartVarName }} {{ .MultipartVarName }} {{ $c.PkgName }}.{{ .MultipartFuncName }}, {{- end }} {{- end }} + {{- if .Interceptors }} + {{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors, + {{- end }} {{- end }} ) (goa.Endpoint, any, error) { {{ .FlagsCode }} @@ -34,11 +37,16 @@ func ParseEndpoint( case "{{ .Name }}": c := {{ .PkgName }}.NewClient(scheme, host, doer, enc, dec, restore{{ if .NeedStream }}, dialer, {{ .VarName }}Configurer{{ end }}) switch epn { - {{- $pkgName := .PkgName }}{{ range .Subcommands }} + {{- $pkgName := .PkgName }} + {{- $interceptors := .Interceptors }} + {{- range .Subcommands }} case "{{ .Name }}": endpoint = c.{{ .MethodVarName }}({{ if .MultipartVarName }}{{ .MultipartVarName }}{{ end }}) + {{- if $interceptors }} + endpoint = {{ $interceptors.PkgName }}.Wrap{{ .MethodVarName }}ClientEndpoint(endpoint, {{ $interceptors.VarName }}) + {{- end }} {{- if .BuildFunction }} - data, err = {{ $pkgName}}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }}) + data, err = {{ $pkgName }}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }}) {{- else if .Conversion }} {{ .Conversion }} {{- end }} diff --git a/pkg/interceptor.go b/pkg/interceptor.go index 022d7a4de6..c784f14b88 100644 --- a/pkg/interceptor.go +++ b/pkg/interceptor.go @@ -1,7 +1,5 @@ package goa -import "context" - type ( // InterceptorInfo contains information about the request shared between // all interceptors in the service chain. It provides access to the service name, @@ -16,7 +14,4 @@ type ( // Payload of request RawPayload any } - - // NextFunc is a function that will continue the request processing chain. - NextFunc func(ctx context.Context) (any, error) ) From 1c5f19c8c0e530998d38683b1d196a80aceb1902 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Sun, 19 Jan 2025 10:52:14 -0800 Subject: [PATCH 5/6] Finalize initial interceptor implementation --- codegen/cli/cli.go | 3 +- .../templates/server_interceptors.go.tpl | 4 - codegen/service/client.go | 2 +- codegen/service/endpoint.go | 25 +- codegen/service/example_interceptors.go | 4 +- codegen/service/interceptors.go | 84 +++--- codegen/service/interceptors.md | 149 +++++++++++ codegen/service/interceptors_test.go | 8 +- codegen/service/service.go | 2 +- codegen/service/service_data.go | 246 +++++++++++------- .../client_interceptor_wrappers.go.tpl | 12 +- .../service/templates/client_wrappers.go.tpl | 5 +- .../templates/endpoint_wrappers.go.tpl | 4 +- codegen/service/templates/interceptors.go.tpl | 144 ++++------ .../templates/interceptors_types.go.tpl | 62 +++++ .../server_interceptor_wrappers.go.tpl | 12 +- .../templates/server_interceptors.go.tpl | 4 +- dsl/interceptor.go | 4 + expr/method.go | 59 ++--- expr/method_test.go | 114 ++------ expr/testdata/interceptors_validate_dsls.go | 83 ++++++ grpc/codegen/client.go | 2 +- grpc/codegen/client_cli.go | 9 +- grpc/codegen/parse_endpoint_test.go | 40 +++ grpc/codegen/templates/parse_endpoint.go.tpl | 2 + .../testdata/client-interceptors.golden | 2 +- grpc/codegen/testdata/dsls.go | 7 + ...endpoint-endpoint-with-interceptors.golden | 178 +++++++++++++ http/codegen/client.go | 2 +- 29 files changed, 889 insertions(+), 383 deletions(-) create mode 100644 codegen/service/interceptors.md create mode 100644 codegen/service/templates/interceptors_types.go.tpl create mode 100644 expr/testdata/interceptors_validate_dsls.go create mode 100644 grpc/codegen/parse_endpoint_test.go create mode 100644 grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden diff --git a/codegen/cli/cli.go b/codegen/cli/cli.go index 1135424ae3..6c852cc5e8 100644 --- a/codegen/cli/cli.go +++ b/codegen/cli/cli.go @@ -702,7 +702,8 @@ const parseFlagsT = `var ( ` // input: commandData -const commandUsageT = `{{ printf "%sUsage displays the usage of the %s command and its subcommands." .Name .Name | comment }} +const commandUsageT = ` +{{ printf "%sUsage displays the usage of the %s command and its subcommands." .VarName .Name | comment }} func {{ .VarName }}Usage() { fmt.Fprintf(os.Stderr, ` + "`" + `{{ printDescription .Description }} Usage: diff --git a/codegen/example/templates/server_interceptors.go.tpl b/codegen/example/templates/server_interceptors.go.tpl index c8bf2215b7..c2b09fcae3 100644 --- a/codegen/example/templates/server_interceptors.go.tpl +++ b/codegen/example/templates/server_interceptors.go.tpl @@ -8,16 +8,12 @@ {{- end }} {{- end }} ) - {{- end }} - {{- if .HasInterceptors }} { - {{- end }} {{- range .Services }} {{- if and .Methods .ServerInterceptors }} {{ .VarName }}Interceptors = {{ $.InterPkg }}.New{{ .StructName }}ServerInterceptors() {{- end }} {{- end }} - {{- if .HasInterceptors }} } {{- end }} {{- end }} \ No newline at end of file diff --git a/codegen/service/client.go b/codegen/service/client.go index a088de0d37..8cc61b2dc4 100644 --- a/codegen/service/client.go +++ b/codegen/service/client.go @@ -15,7 +15,7 @@ const ( // ClientFile returns the client file for the given service. func ClientFile(_ string, service *expr.ServiceExpr) *codegen.File { svc := Services.Get(service.Name) - data := endpointData(service) + data := endpointData(svc) path := filepath.Join(codegen.Gendir, svc.PathName, "client.go") var ( sections []*codegen.SectionTemplate diff --git a/codegen/service/endpoint.go b/codegen/service/endpoint.go index b499a9e6f7..8ca7156704 100644 --- a/codegen/service/endpoint.go +++ b/codegen/service/endpoint.go @@ -50,10 +50,6 @@ type ( ServiceName string // ServiceVarName is the name of the owner service Go interface. ServiceVarName string - // ServerInterceptors contains the server-side interceptors for this method - ServerInterceptors []*InterceptorData - // ClientInterceptors contains the client-side interceptors for this method - ClientInterceptors []*InterceptorData } ) @@ -71,7 +67,7 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File { svc := Services.Get(service.Name) svcName := svc.PathName path := filepath.Join(codegen.Gendir, svcName, "endpoints.go") - data := endpointData(service) + data := endpointData(svc) var ( sections []*codegen.SectionTemplate ) @@ -138,25 +134,22 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File { return &codegen.File{Path: path, SectionTemplates: sections} } -func endpointData(service *expr.ServiceExpr) *EndpointsData { - svc := Services.Get(service.Name) +func endpointData(svc *Data) *EndpointsData { methods := make([]*EndpointMethodData, len(svc.Methods)) names := make([]string, len(svc.Methods)) for i, m := range svc.Methods { methods[i] = &EndpointMethodData{ - MethodData: m, - ArgName: codegen.Goify(m.VarName, false), - ServiceName: svc.Name, - ServiceVarName: serviceInterfaceName, - ClientVarName: clientStructName, - ServerInterceptors: m.ServerInterceptors, - ClientInterceptors: m.ClientInterceptors, + MethodData: m, + ArgName: codegen.Goify(m.VarName, false), + ServiceName: svc.Name, + ServiceVarName: serviceInterfaceName, + ClientVarName: clientStructName, } names[i] = codegen.Goify(m.VarName, false) } - desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, service.Name) + desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, svc.Name) return &EndpointsData{ - Name: service.Name, + Name: svc.Name, Description: desc, VarName: endpointsStructName, ClientVarName: clientStructName, diff --git a/codegen/service/example_interceptors.go b/codegen/service/example_interceptors.go index c348beae57..7c24b08bea 100644 --- a/codegen/service/example_interceptors.go +++ b/codegen/service/example_interceptors.go @@ -49,7 +49,7 @@ func exampleInterceptorsFile(genpkg string, svc *expr.ServiceExpr) []*codegen.Fi {Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName}, }), { - Name: "server-interceptor", + Name: "exmaple-server-interceptor", Source: readTemplate("example_server_interceptor"), Data: data, }, @@ -73,7 +73,7 @@ func exampleInterceptorsFile(genpkg string, svc *expr.ServiceExpr) []*codegen.Fi {Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName}, }), { - Name: "client-interceptor", + Name: "example-client-interceptor", Source: readTemplate("example_client_interceptor"), Data: data, }, diff --git a/codegen/service/interceptors.go b/codegen/service/interceptors.go index 78ddb543ca..8da90f9f1e 100644 --- a/codegen/service/interceptors.go +++ b/codegen/service/interceptors.go @@ -29,35 +29,41 @@ func InterceptorsFiles(genpkg string, service *expr.ServiceExpr) []*codegen.File } // interceptorFile returns the file defining the interceptors. +// This method is called twice, once for the server and once for the client. func interceptorFile(svc *Data, server bool) *codegen.File { filename := "client_interceptors.go" template := "client_interceptors" - section := "client-interceptors" + section := "client-interceptors-type" desc := "Client Interceptors" - var data []*InterceptorData if server { filename = "service_interceptors.go" template = "server_interceptors" - section = "server-interceptors" + section = "server-interceptors-type" desc = "Server Interceptors" - data = svc.ServerInterceptors } desc = svc.Name + desc path := filepath.Join(codegen.Gendir, svc.PathName, filename) + interceptors := svc.ServerInterceptors if !server { - // We don't want to generate duplicate interceptor info data structures for - // interceptors that are both server and client side. - serverInterceptors := make(map[string]struct{}, len(svc.ServerInterceptors)) - for _, intr := range svc.ServerInterceptors { - serverInterceptors[intr.Name] = struct{}{} + interceptors = svc.ClientInterceptors + } + + // We don't want to generate duplicate interceptor info data structures for + // interceptors that are both server and client side so remove interceptors + // that are both server and client side when generating the client. + if !server { + names := make(map[string]struct{}, len(svc.ServerInterceptors)) + for _, sin := range svc.ServerInterceptors { + names[sin.Name] = struct{}{} } - for _, intr := range svc.ClientInterceptors { - if _, ok := serverInterceptors[intr.Name]; ok { - continue + filtered := make([]*InterceptorData, 0, len(interceptors)) + for _, in := range interceptors { + if _, ok := names[in.Name]; !ok { + filtered = append(filtered, in) } - data = append(data, intr) } + interceptors = filtered } sections := []*codegen.SectionTemplate{ @@ -66,45 +72,59 @@ func interceptorFile(svc *Data, server bool) *codegen.File { codegen.GoaImport(""), }), { - Name: "section" + "-struct", + Name: section, Source: readTemplate(template), Data: svc, }, } - if len(data) > 0 { + if len(interceptors) > 0 { sections = append(sections, &codegen.SectionTemplate{ - Name: section, - Source: readTemplate("interceptors"), - Data: data, + Name: "interceptor-types", + Source: readTemplate("interceptors_types"), + Data: interceptors, FuncMap: map[string]any{ "hasPrivateImplementationTypes": hasPrivateImplementationTypes, }, }) } - // Add wrapper sections for each method that has interceptors + template = "endpoint_wrappers" + section = "endpoint-wrapper" + if !server { + template = "client_wrappers" + section = "client-wrapper" + } for _, m := range svc.Methods { - if server && len(m.ServerInterceptors) == 0 || !server && len(m.ClientInterceptors) == 0 { - continue - } - template := "endpoint_wrappers" - templateName := "endpoint-wrapper" + ints := m.ServerInterceptors if !server { - template = "client_wrappers" - templateName = "client-wrapper" + ints = m.ClientInterceptors + } + if len(ints) == 0 { + continue } sections = append(sections, &codegen.SectionTemplate{ - Name: templateName, + Name: section, Source: readTemplate(template), Data: map[string]interface{}{ - "MethodVarName": codegen.Goify(m.Name, true), - "Method": m.Name, - "Service": svc.Name, - "ServerInterceptors": m.ServerInterceptors, - "ClientInterceptors": m.ClientInterceptors, + "MethodVarName": m.VarName, + "Method": m.Name, + "Service": svc.Name, + "Interceptors": ints, }, }) } + + if len(interceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "interceptors", + Source: readTemplate("interceptors"), + Data: interceptors, + FuncMap: map[string]any{ + "hasPrivateImplementationTypes": hasPrivateImplementationTypes, + }, + }) + } + return &codegen.File{Path: path, SectionTemplates: sections} } diff --git a/codegen/service/interceptors.md b/codegen/service/interceptors.md new file mode 100644 index 0000000000..80795d3d56 --- /dev/null +++ b/codegen/service/interceptors.md @@ -0,0 +1,149 @@ +# Interceptors Code Generation + +Goa generates interceptor code to enable request/response interception and payload/result access. + +--- + +## 1. Client & Server Interceptors + +### Where They Are Generated + +Client and server interceptor code is generated in: + +- `gen/client_interceptors.go` +- `gen/service_interceptors.go` + +### Templates Used + +1. **`server_interceptors.go.tpl`** and **`client_interceptors.go.tpl`** + Define the server and client interceptor interface types. + Each template takes a `Data` struct as input. + +2. **`interceptors.go.tpl`** + Generates the payload/result access interfaces and accompanying info structs. + This template takes a slice of `InterceptorData` as input. + +3. **`client_wrappers.go.tpl`** and **`endpoint_wrappers.go.tpl`** + Generate code that wraps client and service endpoints with interceptor callbacks. + Each template takes a map with: + ```go + map[string]any{ + "MethodVarName": + "Method": + "Service": + "Interceptors": + } + ``` + +--- + +## 2. Client & Server Endpoint Wrapper Code + +### Where It Is Generated + +Endpoint wrapper code for both client and server interceptors is generated in: + +- `gen/interceptor_wrappers.go` + +### Templates Used + +1. **`server_interceptor_wrappers.go.tpl`** + Generates server-specific wrapper implementations. This template receives a map with: + ```go + map[string]any{ + "Service": svc.Name, + "ServerInterceptors": svc.ServerInterceptors, + } + ``` + +2. **`client_interceptor_wrappers.go.tpl`** + Generates client-specific wrapper implementations. This template receives a map with: + ```go + map[string]any{ + "Service": svc.Name, + "ClientInterceptors": svc.ClientInterceptors, + } + ``` + +## 3. Example Interceptors + +### Where They Are Generated + +Example interceptors are generated by the example command in an interceptors sub-package of the user’s service package: + +* Client interceptors: `_client.go` +* Server interceptors: `_server.go` + +### Templates Used + +1. **`example_client_interceptor.go.tpl` and `example_server_interceptor.go.tpl`** + Generate example interceptor implementations. Each template takes a map with: + ```go + map[string]any{ + "StructName": + "ServiceName": + "PkgName": + "ServerInterceptors": + "ClientInterceptors": + } + ``` + +2. **`example_service_init.go.tpl`** + Generates an example service implementation. This template takes a Data struct. + +### Generated Example Features + +The example interceptors demonstrate common interception patterns: + +* Logging of request/response data +* Error handling and error logging +* Type-safe payload and result access through the generated info structs +* Context propagation across requests and responses + +## 4. Data Structures + +The templates above share a common set of data structures that describe +interceptor behavior, payload, result access, and streaming. These data +structures are defined as follows: + +### `Data` + +A service-level structure containing information for both client and server interceptors: + +* `ServerInterceptors`: A slice of InterceptorData for server-side interceptors +* `ClientInterceptors`: A slice of InterceptorData for client-side interceptors + +### `InterceptorData` + +The main structure describing each interceptor’s metadata and requirements: + +* `Name`: Generated Go name of the interceptor (CamelCase) +* `DesignName`: Original name from the design +* `Description`: Interceptor description from the design +* `HasPayloadAccess`: Indicates if any method requires payload access +* `HasResultAccess`: Indicates if any method requires result access +* `ReadPayload`: List of readable payload fields ([]AttributeData) +* `WritePayload`: List of writable payload fields ([]AttributeData) +* `ReadResult`: List of readable result fields ([]AttributeData) +* `WriteResult`: List of writable result fields ([]AttributeData) +* `Methods`: A list of MethodInterceptorData containing method-specific interceptor information +* `ServerStreamInputStruct`: Server stream variable name (used if streaming) +* `ClientStreamInputStruct`: Client stream variable name (used if streaming) + +### `MethodInterceptorData` + +Stores per-method interceptor configuration: + +* `MethodName`: The method’s Go variable name +* `PayloadAccess`: Name of the payload access type +* `ResultAccess`: Name of the result access type +* `PayloadRef`: Reference to the method's payload type +* `ResultRef`: Reference to the method's result type + +### `AttributeData` + +Represents per-field access configuration: + +* `Name`: The field accessor method name +* `TypeRef`: Go type reference for the field +* `Type`: Underlying attribute type information diff --git a/codegen/service/interceptors_test.go b/codegen/service/interceptors_test.go index e484535c9a..6fa7c3ab14 100644 --- a/codegen/service/interceptors_test.go +++ b/codegen/service/interceptors_test.go @@ -125,7 +125,7 @@ func TestCollectAttributes(t *testing.T) { Validation: &expr.ValidationExpr{Required: []string{"name"}}, }, want: []*AttributeData{ - {Name: "Name", TypeRef: "string", FieldPointer: false}, + {Name: "Name", TypeRef: "string", Pointer: false}, }, }, { @@ -141,7 +141,7 @@ func TestCollectAttributes(t *testing.T) { }, }, want: []*AttributeData{ - {Name: "Age", TypeRef: "int", FieldPointer: true}, + {Name: "Age", TypeRef: "int", Pointer: true}, }, }, { @@ -160,8 +160,8 @@ func TestCollectAttributes(t *testing.T) { Validation: &expr.ValidationExpr{Required: []string{"name"}}, }, want: []*AttributeData{ - {Name: "Name", TypeRef: "string", FieldPointer: false}, - {Name: "Age", TypeRef: "int", FieldPointer: true}, + {Name: "Name", TypeRef: "string", Pointer: false}, + {Name: "Age", TypeRef: "int", Pointer: true}, }, }, { diff --git a/codegen/service/service.go b/codegen/service/service.go index 5fc68eb860..706ea255ad 100644 --- a/codegen/service/service.go +++ b/codegen/service/service.go @@ -197,7 +197,7 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s // service and client interceptors files = append(files, InterceptorsFiles(genpkg, service)...) - + // user types paths := make([]string, len(typeDefSections)) i := 0 diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go index 9451d05df7..59d7886ea9 100644 --- a/codegen/service/service_data.go +++ b/codegen/service/service_data.go @@ -64,11 +64,11 @@ type ( Methods []*MethodData // Schemes is the list of security schemes required by the service methods. Schemes SchemesData - // ServerInterceptors is the union of the server interceptors defined - // at the methods, service and the API level. + // ServerInterceptors contains the data needed to render the server-side + // interceptors code. ServerInterceptors []*InterceptorData - // ClientInterceptors is the union of the client interceptors defined - // at the methods, service and the API level. + // ClientInterceptors contains the data needed to render the client-side + // interceptors code. ClientInterceptors []*InterceptorData // Scope initialized with all the service types. Scope *codegen.NameScope @@ -158,10 +158,10 @@ type ( Schemes SchemesData // ServerInterceptors list the server interceptors that apply to this // method. - ServerInterceptors []*InterceptorData + ServerInterceptors []string // ClientInterceptors list the client interceptors that apply to this // method. - ClientInterceptors []*InterceptorData + ClientInterceptors []string // ViewedResult contains the data required to generate the code handling // views if any. ViewedResult *ViewedResultTypeData @@ -247,41 +247,56 @@ type ( Fault bool } - // InterceptorData describes a single interceptor that can read and write - // payload and result attributes. + // InterceptorData contains the data required to render the service-level + // interceptor code. interceptors.go.tpl InterceptorData struct { // Name is the name of the interceptor used in the generated code. Name string // DesignName is the name of the interceptor as defined in the design. DesignName string - // UnexportedName is the private version of the interceptor name used in - // generated code. - UnexportedName string // Description is the description of the interceptor from the design. Description string - // PayloadRef is the reference to the payload type that the interceptor - // can access. - PayloadRef string - // ResultRef is the reference to the result type that the interceptor - // can access. - ResultRef string - // ReadPayload lists the payload attributes that the interceptor can - // read. These are defined using ReadPayload() in the design DSL. + // Methods + Methods []*MethodInterceptorData + // ReadPayload contains payload attributes that the interceptor can + // read. ReadPayload []*AttributeData - // WritePayload lists the payload attributes that the interceptor can - // write. These are defined using WritePayload() in the design DSL. + // WritePayload contains payload attributes that the interceptor can + // write. WritePayload []*AttributeData - // ReadResult lists the result attributes that the interceptor can read. - // These are defined using ReadResult() in the design DSL. + // ReadResult contains result attributes that the interceptor can read. ReadResult []*AttributeData - // WriteResult lists the result attributes that the interceptor can - // write. These are defined using WriteResult() in the design DSL. + // WriteResult contains result attributes that the interceptor can + // write. WriteResult []*AttributeData - // ServerStreamInputStruct is the name of the server stream input struct - // type used when the interceptor is applied to a streaming endpoint. + // HasPayloadAccess indicates that the interceptor info object has a + // payload access interface. + HasPayloadAccess bool + // HasResultAccess indicates that the interceptor info object has a + // result access interface. + HasResultAccess bool + } + + // MethodInterceptorData contains the data required to render the + // method-level interceptor code. + MethodInterceptorData struct { + // Name is the name of the interceptor. + Name string + // MethodName is the name of the method. + MethodName string + // PayloadAccess is the name of the payload access struct. + PayloadAccess string + // ResultAccess is the name of the result access struct. + ResultAccess string + // PayloadRef is the reference to the method payload type. + PayloadRef string + // ResultRef is the reference to the method result type. + ResultRef string + // ServerStreamInputStruct is the name of the server stream input + // struct if the endpoint defines a server stream. ServerStreamInputStruct string - // ClientStreamInputStruct is the name of the client stream input struct - // type used when the interceptor is applied to a streaming endpoint. + // ClientStreamInputStruct is the name of the client stream input + // struct if the endpoint defines a client stream. ClientStreamInputStruct string } @@ -291,8 +306,8 @@ type ( Name string // TypeRef is the reference to the attribute type. TypeRef string - // FieldPointer is true if the attribute is a pointer. - FieldPointer bool + // Pointer is true if the attribute is a pointer. + Pointer bool } // RequirementsData is the list of security requirements. @@ -806,17 +821,6 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { } } - var ( - serverInterceptors []*InterceptorData - clientInterceptors []*InterceptorData - seenServer = make(map[string]struct{}) - seenClient = make(map[string]struct{}) - ) - for _, m := range methods { - serverInterceptors = append(serverInterceptors, collectInterceptors(m.ServerInterceptors, seenServer)...) - clientInterceptors = append(clientInterceptors, collectInterceptors(m.ClientInterceptors, seenClient)...) - } - var ( desc string ) @@ -840,8 +844,8 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { ViewsPkg: viewspkg, Methods: methods, Schemes: schemes, - ServerInterceptors: serverInterceptors, - ClientInterceptors: clientInterceptors, + ServerInterceptors: collectInterceptors(service, methods, scope, true), + ClientInterceptors: collectInterceptors(service, methods, scope, false), Scope: scope, ViewScope: viewScope, errorTypes: errTypes, @@ -852,11 +856,47 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { viewedResultTypes: viewedRTs, unionValueMethods: unionMethods, } + d[service.Name] = data return data } +// collectInterceptors returns the set of interceptors defined on the given +// service including any interceptor defined on specific service methods or API. +func collectInterceptors(svc *expr.ServiceExpr, methods []*MethodData, scope *codegen.NameScope, server bool) []*InterceptorData { + var ints []*expr.InterceptorExpr + if server { + ints = expr.Root.API.ServerInterceptors + ints = append(ints, svc.ServerInterceptors...) + for _, m := range svc.Methods { + ints = append(ints, m.ServerInterceptors...) + } + } else { + ints = expr.Root.API.ClientInterceptors + ints = append(ints, svc.ClientInterceptors...) + for _, m := range svc.Methods { + ints = append(ints, m.ClientInterceptors...) + } + } + // remove duplicate interceptors + sort.Slice(ints, func(i, j int) bool { + return ints[i].Name < ints[j].Name + }) + for i := 1; i < len(ints); i++ { + if ints[i-1].Name == ints[i].Name { + ints = append(ints[:i], ints[i+1:]...) + i-- + } + } + + res := make([]*InterceptorData, 0, len(ints)) + for _, i := range ints { + res = append(res, buildInterceptorData(svc, methods, i, scope, server)) + } + return res +} + // typeContext returns a contextual attribute for service types. Service types // are Go types and uses non-pointers to hold attributes having default values. func typeContext(pkg string, scope *codegen.NameScope) *codegen.AttributeContext { @@ -949,18 +989,6 @@ func collectUnionMethods(att *expr.AttributeExpr, scope *codegen.NameScope, loc return } -// collectInterceptors traverses the interceptors to build a unique set. -func collectInterceptors(interceptors []*InterceptorData, seen map[string]struct{}) (data []*InterceptorData) { - for _, i := range interceptors { - if _, ok := seen[i.Name]; ok { - continue - } - seen[i.Name] = struct{}{} - data = append(data, i) - } - return -} - // buildErrorInitData creates the data needed to generate code around endpoint error return values. func buildErrorInitData(er *expr.ErrorExpr, scope *codegen.NameScope) *ErrorInitData { _, temporary := er.AttributeExpr.Meta["goa:error:temporary"] @@ -1087,7 +1115,6 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData { ResponseStruct: vname + "ResponseData", } initStreamData(data, m, vname, rname, resultRef, scope) - initInterceptorsData(data, m, payloadRef, resultRef, scope) return data } @@ -1169,38 +1196,77 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe data.StreamingPayloadEx = spayloadEx } -func initInterceptorsData(data *MethodData, m *expr.MethodExpr, payloadRef, resultRef string, scope *codegen.NameScope) { - if len(m.ServerInterceptors) == 0 && len(m.ClientInterceptors) == 0 { - return - } - buildInterceptorData := func(i *expr.InterceptorExpr, serverStreamStruct, clientStreamStruct string, scope *codegen.NameScope) *InterceptorData { - return &InterceptorData{ - Name: codegen.Goify(i.Name, true), - DesignName: i.Name, - UnexportedName: codegen.Goify(i.Name, false), - Description: i.Description, - PayloadRef: payloadRef, - ResultRef: resultRef, - ServerStreamInputStruct: serverStreamStruct, - ClientStreamInputStruct: clientStreamStruct, - ReadPayload: collectAttributes(i.ReadPayload, m.Payload, scope), - WritePayload: collectAttributes(i.WritePayload, m.Payload, scope), - ReadResult: collectAttributes(i.ReadResult, m.Result, scope), - WriteResult: collectAttributes(i.WriteResult, m.Result, scope), +// buildInterceptorData creates the data needed to generate interceptor code. +func buildInterceptorData(svc *expr.ServiceExpr, methods []*MethodData, i *expr.InterceptorExpr, scope *codegen.NameScope, server bool) *InterceptorData { + data := &InterceptorData{ + Name: codegen.Goify(i.Name, true), + DesignName: i.Name, + Description: i.Description, + } + if len(svc.Methods) > 0 { + payload, result := svc.Methods[0].Payload, svc.Methods[0].Result + data.ReadPayload = collectAttributes(i.ReadPayload, payload, scope) + data.WritePayload = collectAttributes(i.WritePayload, payload, scope) + data.ReadResult = collectAttributes(i.ReadResult, result, scope) + data.WriteResult = collectAttributes(i.WriteResult, result, scope) + if len(data.ReadPayload) > 0 || len(data.WritePayload) > 0 { + data.HasPayloadAccess = true + } + if len(data.ReadResult) > 0 || len(data.WriteResult) > 0 { + data.HasResultAccess = true + } + for _, m := range svc.Methods { + applies := false + intExprs := m.ServerInterceptors + if !server { + intExprs = m.ClientInterceptors + } + for _, in := range intExprs { + if in.Name == i.Name { + applies = true + break + } + } + if !applies { + continue + } + var md *MethodData + for _, mt := range methods { + if m.Name == mt.Name { + md = mt + break + } + } + data.Methods = append(data.Methods, buildInterceptorMethodData(i, md)) + if server { + md.ServerInterceptors = append(md.ServerInterceptors, i.Name) + } else { + md.ClientInterceptors = append(md.ClientInterceptors, i.Name) + } } } - var serverEndpointStruct, clientEndpointStruct string - if data.ServerStream != nil { - serverEndpointStruct = data.ServerStream.EndpointStruct - } - if data.ClientStream != nil { - clientEndpointStruct = data.ClientStream.EndpointStruct - } - for _, i := range m.ServerInterceptors { - data.ServerInterceptors = append(data.ServerInterceptors, buildInterceptorData(i, serverEndpointStruct, clientEndpointStruct, scope)) - } - for _, i := range m.ClientInterceptors { - data.ClientInterceptors = append(data.ClientInterceptors, buildInterceptorData(i, serverEndpointStruct, clientEndpointStruct, scope)) + return data +} + +// buildIntercetorMethodData creates the data needed to generate interceptor +// method code. +func buildInterceptorMethodData(i *expr.InterceptorExpr, md *MethodData) *MethodInterceptorData { + var serverStream, clientStream string + if md.ServerStream != nil { + serverStream = md.ServerStream.VarName + } + if md.ClientStream != nil { + clientStream = md.ClientStream.VarName + } + return &MethodInterceptorData{ + Name: i.Name, + MethodName: md.VarName, + PayloadAccess: codegen.Goify(i.Name, false) + md.VarName + "Payload", + ResultAccess: codegen.Goify(i.Name, false) + md.VarName + "Result", + PayloadRef: md.PayloadRef, + ResultRef: md.ResultRef, + ClientStreamInputStruct: clientStream, + ServerStreamInputStruct: serverStream, } } @@ -1322,9 +1388,9 @@ func collectAttributes(attrNames, parent *expr.AttributeExpr, scope *codegen.Nam continue } data[i] = &AttributeData{ - Name: codegen.Goify(nat.Name, true), - TypeRef: scope.GoTypeRef(parentAttr), - FieldPointer: parent.IsPrimitivePointer(nat.Name, true), + Name: codegen.Goify(nat.Name, true), + TypeRef: scope.GoTypeRef(parentAttr), + Pointer: parent.IsPrimitivePointer(nat.Name, true), } } return data diff --git a/codegen/service/templates/client_interceptor_wrappers.go.tpl b/codegen/service/templates/client_interceptor_wrappers.go.tpl index f2f63223db..5cd5f9d9d1 100644 --- a/codegen/service/templates/client_interceptor_wrappers.go.tpl +++ b/codegen/service/templates/client_interceptor_wrappers.go.tpl @@ -1,10 +1,13 @@ {{- range .ClientInterceptors }} -{{ comment (printf "wrapClient%s applies the %s interceptor to endpoints." .Name .DesignName) }} -func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +{{- $interceptor := . }} +{{- range .Methods }} + +{{ comment (printf "wrapClient%s%s applies the %s client interceptor to endpoints." $interceptor.Name .MethodName $interceptor.DesignName) }} +func wrapClient{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { - info := &{{ .Name }}Info{ + info := &{{ $interceptor.Name }}Info{ Service: "{{ $.Service }}", - Method: method, + Method: "{{ .MethodName }}", Endpoint: endpoint, {{- if .ClientStreamInputStruct }} RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload, @@ -15,4 +18,5 @@ func wrapClient{{ .Name }}(endpoint goa.Endpoint, i ClientInterceptors, method s return i.{{ .Name }}(ctx, info, endpoint) } } +{{ end }} {{- end }} diff --git a/codegen/service/templates/client_wrappers.go.tpl b/codegen/service/templates/client_wrappers.go.tpl index 6b05597c00..9287d25b35 100644 --- a/codegen/service/templates/client_wrappers.go.tpl +++ b/codegen/service/templates/client_wrappers.go.tpl @@ -1,7 +1,8 @@ + {{ comment (printf "Wrap%sClientEndpoint wraps the %s endpoint with the client interceptors defined in the design." .MethodVarName .Method) }} func Wrap{{ .MethodVarName }}ClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - {{- range .ClientInterceptors }} - endpoint = wrapClient{{ .Name }}(endpoint, i, "{{ $.Method }}") + {{- range .Interceptors }} + endpoint = wrapClient{{ $.MethodVarName }}{{ . }}(endpoint, i) {{- end }} return endpoint } diff --git a/codegen/service/templates/endpoint_wrappers.go.tpl b/codegen/service/templates/endpoint_wrappers.go.tpl index 8c82741cc9..deffd14fc9 100644 --- a/codegen/service/templates/endpoint_wrappers.go.tpl +++ b/codegen/service/templates/endpoint_wrappers.go.tpl @@ -1,7 +1,7 @@ {{ comment (printf "Wrap%sEndpoint wraps the %s endpoint with the server-side interceptors defined in the design." .MethodVarName .Method) }} func Wrap{{ .MethodVarName }}Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - {{- range .ServerInterceptors }} - endpoint = wrap{{ .Name }}(endpoint, i, "{{ $.Method }}") + {{- range .Interceptors }} + endpoint = wrap{{ $.MethodVarName }}{{ . }}(endpoint, i) {{- end }} return endpoint } diff --git a/codegen/service/templates/interceptors.go.tpl b/codegen/service/templates/interceptors.go.tpl index 5f2024cf33..ea3b0b5e86 100644 --- a/codegen/service/templates/interceptors.go.tpl +++ b/codegen/service/templates/interceptors.go.tpl @@ -1,128 +1,96 @@ - -// Access interfaces for interceptor payloads and results -type ( -{{- range . }} - // {{ .Name }}Info provides metadata about the current interception. - // It includes service name, method name, and access to the endpoint. - {{ .Name }}Info goa.InterceptorInfo - {{- if or .ReadPayload .WritePayload }} - - // {{ .Name }}PayloadAccess provides type-safe access to the method payload. - // It allows reading and writing specific fields of the payload as defined - // in the design. - {{ .Name }}PayloadAccess interface { - {{- range .ReadPayload }} - {{ .Name }}() {{ .TypeRef }} - {{- end }} - {{- range .WritePayload }} - Set{{ .Name }}({{ .TypeRef }}) - {{- end }} - } - {{- end }} - {{- if or .ReadResult .WriteResult }} - - // {{ .Name }}ResultAccess provides type-safe access to the method result. - // It allows reading and writing specific fields of the result as defined - // in the design. - {{ .Name }}ResultAccess interface { - {{- range .ReadResult }} - {{ .Name }}() {{ .TypeRef }} - {{- end }} - {{- range .WriteResult }} - Set{{ .Name }}({{ .TypeRef }}) - {{- end }} - } - {{- end }} -{{- end }} -) - {{- if hasPrivateImplementationTypes . }} - -// Private implementation types -type ( - {{- range . }} - {{- if or .ReadPayload .WritePayload }} - {{ .UnexportedName }}PayloadAccess struct { - payload {{ .PayloadRef }} - } - {{- end }} - - {{- if or .ReadResult .WriteResult }} - {{ .UnexportedName }}ResultAccess struct { - result {{ .ResultRef }} - } - {{- end }} - {{- end }} -) - // Public accessor methods for Info types {{- range . }} - {{- if or .ReadPayload .WritePayload }} + {{- if .HasPayloadAccess }} + // Payload returns a type-safe accessor for the method payload. -func (info *{{ .Name }}Info) Payload() {{ .Name }}PayloadAccess { - return &{{ .UnexportedName }}PayloadAccess{payload: info.RawPayload.({{ .PayloadRef }})} +func (info *{{ .Name }}Info) Payload() {{ .Name }}Payload { + {{- if gt (len .Methods) 1 }} + switch info.Method { + {{- range .Methods }} + case "{{ .MethodName }}": + return &{{ .PayloadAccess }}{payload: info.RawPayload.({{ .PayloadRef }})} + {{- end }} + default: + return nil + } + {{- else }} + return &{{ (index .Methods 0).PayloadAccess }}{payload: info.RawPayload.({{ (index .Methods 0).PayloadRef }})} + {{- end }} } {{- end }} - {{- if or .ReadResult .WriteResult }} + {{- if .HasResultAccess }} // Result returns a type-safe accessor for the method result. -func (info *{{ .Name }}Info) Result(res any) {{ .Name }}ResultAccess { - return &{{ .UnexportedName }}ResultAccess{result: res.({{ .ResultRef }})} +func (info *{{ .Name }}Info) Result(res any) {{ .Name }}Result { + {{- if gt (len .Methods) 1 }} + switch info.Method { + {{- range .Methods }} + case "{{ .MethodName }}": + return &{{ .ResultAccess }}{result: res.({{ .ResultRef }})} + {{- end }} + default: + return nil + } + {{- else }} + return &{{ (index .Methods 0).ResultAccess }}{result: res.({{ (index .Methods 0).ResultRef }})} + {{- end }} } {{- end }} {{- end }} // Private implementation methods {{- range . }} - {{- $interceptor := . }} - {{- range .ReadPayload }} -func (p *{{ $interceptor.UnexportedName }}PayloadAccess) {{ .Name }}() {{ .TypeRef }} { - {{- if .FieldPointer }} + {{ $interceptor := . }} + {{- range .Methods }} + {{- $method := . }} + {{- range $interceptor.ReadPayload }} +func (p *{{ $method.PayloadAccess }}) {{ .Name }}() {{ .TypeRef }} { + {{- if .Pointer }} if p.payload.{{ .Name }} == nil { var zero {{ .TypeRef }} return zero } return *p.payload.{{ .Name }} - {{- else }} + {{- else }} return p.payload.{{ .Name }} - {{- end }} + {{- end }} } - {{- end }} + {{- end }} - {{- range .WritePayload }} -func (p *{{ $interceptor.UnexportedName }}PayloadAccess) Set{{ .Name }}(v {{ .TypeRef }}) { - {{- if .FieldPointer }} + {{- range $interceptor.WritePayload }} +func (p *{{ $method.PayloadAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .Pointer }} p.payload.{{ .Name }} = &v - {{- else }} + {{- else }} p.payload.{{ .Name }} = v - {{- end }} + {{- end }} } - {{- end }} + {{- end }} - {{- range .ReadResult }} -func (r *{{ $interceptor.UnexportedName }}ResultAccess) {{ .Name }}() {{ .TypeRef }} { - {{- if .FieldPointer }} + {{- range $interceptor.ReadResult }} +func (r *{{ $method.ResultAccess }}) {{ .Name }}() {{ .TypeRef }} { + {{- if .Pointer }} if r.result.{{ .Name }} == nil { var zero {{ .TypeRef }} return zero } return *r.result.{{ .Name }} - {{- else }} + {{- else }} return r.result.{{ .Name }} - {{- end }} + {{- end }} } - {{- end }} + {{- end }} - {{- range .WriteResult }} -func (r *{{ $interceptor.UnexportedName }}ResultAccess) Set{{ .Name }}(v {{ .TypeRef }}) { - {{- if .FieldPointer }} + {{- range $interceptor.WriteResult }} +func (r *{{ $method.ResultAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .Pointer }} r.result.{{ .Name }} = &v - {{- else }} + {{- else }} r.result.{{ .Name }} = v - {{- end }} + {{- end }} } + {{- end }} {{- end }} {{- end }} {{- end }} - - diff --git a/codegen/service/templates/interceptors_types.go.tpl b/codegen/service/templates/interceptors_types.go.tpl new file mode 100644 index 0000000000..73a159d170 --- /dev/null +++ b/codegen/service/templates/interceptors_types.go.tpl @@ -0,0 +1,62 @@ + +// Access interfaces for interceptor payloads and results +type ( +{{- range . }} + // {{ .Name }}Info provides metadata about the current interception. + // It includes service name, method name, and access to the endpoint. + {{ .Name }}Info goa.InterceptorInfo + {{- if .HasPayloadAccess }} + + // {{ .Name }}Payload provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + {{ .Name }}Payload interface { + {{- range .ReadPayload }} + {{ .Name }}() {{ .TypeRef }} + {{- end }} + {{- range .WritePayload }} + Set{{ .Name }}({{ .TypeRef }}) + {{- end }} + } + {{- end }} + {{- if .HasResultAccess }} + + // {{ .Name }}Result provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + {{ .Name }}Result interface { + {{- range .ReadResult }} + {{ .Name }}() {{ .TypeRef }} + {{- end }} + {{- range .WriteResult }} + Set{{ .Name }}({{ .TypeRef }}) + {{- end }} + } + {{- end }} +{{- end }} +) +{{- if hasPrivateImplementationTypes . }} + +// Private implementation types +type ( + {{- range . }} + {{- range .Methods }} + {{- if .PayloadAccess }} + {{ .PayloadAccess }} struct { + payload {{ .PayloadRef }} + } + {{- end }} + {{- end }} + {{- end }} + + {{- range . }} + {{- range .Methods }} + {{- if .ResultAccess }} + {{ .ResultAccess }} struct { + result {{ .ResultRef }} + } + {{- end }} + {{- end }} + {{- end }} +) +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/server_interceptor_wrappers.go.tpl b/codegen/service/templates/server_interceptor_wrappers.go.tpl index 4971d6510e..3086dae3cf 100644 --- a/codegen/service/templates/server_interceptor_wrappers.go.tpl +++ b/codegen/service/templates/server_interceptor_wrappers.go.tpl @@ -1,10 +1,13 @@ {{- range .ServerInterceptors }} -{{ comment (printf "wrap%s applies the %s interceptor to endpoints." .Name .DesignName) }} -func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { +{{- $interceptor := . }} +{{- range .Methods }} + +{{ comment (printf "wrap%s%s applies the %s server interceptor to endpoints." $interceptor.Name .MethodName $interceptor.DesignName) }} +func wrap{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { - info := &{{ .Name }}Info{ + info := &{{ $interceptor.Name }}Info{ Service: "{{ $.Service }}", - Method: method, + Method: "{{ .MethodName }}", Endpoint: endpoint, {{- if .ServerStreamInputStruct }} RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload, @@ -16,3 +19,4 @@ func wrap{{ .Name }}(endpoint goa.Endpoint, i ServerInterceptors, method string) } } {{- end }} +{{- end }} \ No newline at end of file diff --git a/codegen/service/templates/server_interceptors.go.tpl b/codegen/service/templates/server_interceptors.go.tpl index c19597a383..cdfb9287ec 100644 --- a/codegen/service/templates/server_interceptors.go.tpl +++ b/codegen/service/templates/server_interceptors.go.tpl @@ -4,9 +4,9 @@ // next to complete the request. type ServerInterceptors interface { {{- range .ServerInterceptors }} -{{- if .Description }} + {{- if .Description }} {{ comment .Description }} -{{- end }} + {{- end }} {{ .Name }}(ctx context.Context, info *{{ .Name }}Info, next goa.Endpoint) (any, error) {{- end }} } \ No newline at end of file diff --git a/dsl/interceptor.go b/dsl/interceptor.go index 28b99ce7dd..fdb42fa1b8 100644 --- a/dsl/interceptor.go +++ b/dsl/interceptor.go @@ -251,6 +251,10 @@ func addInterceptors(interceptors []any, client bool) { case *expr.InterceptorExpr: ints = append(ints, i) case string: + if i == "" { + eval.ReportError("%s: interceptor name cannot be empty", kind) + return + } var found bool for _, in := range expr.Root.Interceptors { if in.Name == i { diff --git a/expr/method.go b/expr/method.go index 94e3e59356..0183545e0e 100644 --- a/expr/method.go +++ b/expr/method.go @@ -100,39 +100,6 @@ func (m *MethodExpr) Prepare() { if m.Result == nil { m.Result = &AttributeExpr{Type: Empty} } - - m.ClientInterceptors = mergeInterceptors(m.ClientInterceptors, m.Service.ClientInterceptors, Root.API.ClientInterceptors) - m.ServerInterceptors = mergeInterceptors(m.ServerInterceptors, m.Service.ServerInterceptors, Root.API.ServerInterceptors) -} - -// mergeInterceptors merges interceptors from different levels (method, service, API) -// while avoiding duplicates. The order of precedence is: method > service > API. -func mergeInterceptors(methodLevel, serviceLevel, apiLevel []*InterceptorExpr) []*InterceptorExpr { - existing := make(map[string]struct{}) - result := make([]*InterceptorExpr, 0, len(methodLevel)+len(serviceLevel)+len(apiLevel)) - - // Add method-level interceptors - for _, i := range methodLevel { - existing[i.Name] = struct{}{} - result = append(result, i) - } - - // Add service-level interceptors - for _, i := range serviceLevel { - if _, ok := existing[i.Name]; !ok { - result = append(result, i) - existing[i.Name] = struct{}{} - } - } - - // Add API-level interceptors - for _, i := range apiLevel { - if _, ok := existing[i.Name]; !ok { - result = append(result, i) - } - } - - return result } // Validate validates the method payloads, results, errors, security @@ -272,15 +239,41 @@ func (m *MethodExpr) validateErrors() *eval.ValidationErrors { // validateInterceptors validates the method interceptors. func (m *MethodExpr) validateInterceptors() *eval.ValidationErrors { verr := new(eval.ValidationErrors) + m.ClientInterceptors = mergeInterceptors(m.ClientInterceptors, m.Service.ClientInterceptors, Root.API.ClientInterceptors) for _, i := range m.ClientInterceptors { verr.Merge(i.validate(m)) } + m.ServerInterceptors = mergeInterceptors(m.ServerInterceptors, m.Service.ServerInterceptors, Root.API.ServerInterceptors) for _, i := range m.ServerInterceptors { verr.Merge(i.validate(m)) } return verr } +// mergeInterceptors merges interceptors from different levels (method, service, API) +// while avoiding duplicates. The order of precedence is: method > service > API. +func mergeInterceptors(methodLevel, serviceLevel, apiLevel []*InterceptorExpr) []*InterceptorExpr { + existing := make(map[string]struct{}) + result := make([]*InterceptorExpr, 0, len(methodLevel)+len(serviceLevel)+len(apiLevel)) + + for _, i := range methodLevel { + existing[i.Name] = struct{}{} + result = append(result, i) + } + for _, i := range serviceLevel { + if _, ok := existing[i.Name]; !ok { + result = append(result, i) + existing[i.Name] = struct{}{} + } + } + for _, i := range apiLevel { + if _, ok := existing[i.Name]; !ok { + result = append(result, i) + } + } + return result +} + // hasTag is a helper function that traverses the given attribute and all its // bases recursively looking for an attribute with the given tag meta. This // recursion is only needed for attributes that have not been finalized yet. diff --git a/expr/method_test.go b/expr/method_test.go index d23d58ddc7..6d57459665 100644 --- a/expr/method_test.go +++ b/expr/method_test.go @@ -155,102 +155,30 @@ func TestMethodExprIsPayloadStreaming(t *testing.T) { } } -func TestMethodExprPrepare(t *testing.T) { - var ( - apiInterceptor = &expr.InterceptorExpr{Name: "api"} - svcInterceptor = &expr.InterceptorExpr{Name: "service"} - mtdInterceptor = &expr.InterceptorExpr{Name: "method"} - ) - - cases := map[string]struct { - api *expr.APIExpr - service *expr.ServiceExpr - method *expr.MethodExpr - expectedServer []*expr.InterceptorExpr - expectedClient []*expr.InterceptorExpr +func TestMethodExprValidateInterceptors(t *testing.T) { + cases := []struct { + Name string + DSL func() + Error string }{ - "no-interceptors": { - api: &expr.APIExpr{}, - service: &expr.ServiceExpr{}, - method: &expr.MethodExpr{}, - expectedServer: []*expr.InterceptorExpr{}, - expectedClient: []*expr.InterceptorExpr{}, - }, - "api-only": { - api: &expr.APIExpr{ - ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - }, - service: &expr.ServiceExpr{}, - method: &expr.MethodExpr{}, - expectedServer: []*expr.InterceptorExpr{apiInterceptor}, - expectedClient: []*expr.InterceptorExpr{apiInterceptor}, - }, - "service-only": { - api: &expr.APIExpr{}, - service: &expr.ServiceExpr{ - ServerInterceptors: []*expr.InterceptorExpr{svcInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{svcInterceptor}, - }, - method: &expr.MethodExpr{}, - expectedServer: []*expr.InterceptorExpr{svcInterceptor}, - expectedClient: []*expr.InterceptorExpr{svcInterceptor}, - }, - "method-only": { - api: &expr.APIExpr{}, - service: &expr.ServiceExpr{}, - method: &expr.MethodExpr{ - ServerInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, - }, - expectedServer: []*expr.InterceptorExpr{mtdInterceptor}, - expectedClient: []*expr.InterceptorExpr{mtdInterceptor}, - }, - "chained-interceptors": { - api: &expr.APIExpr{ - ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - }, - service: &expr.ServiceExpr{ - ServerInterceptors: []*expr.InterceptorExpr{svcInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{svcInterceptor}, - }, - method: &expr.MethodExpr{ - ServerInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{mtdInterceptor}, - }, - expectedServer: []*expr.InterceptorExpr{mtdInterceptor, svcInterceptor, apiInterceptor}, - expectedClient: []*expr.InterceptorExpr{mtdInterceptor, svcInterceptor, apiInterceptor}, - }, - "duplicate-interceptors": { - api: &expr.APIExpr{ - ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - }, - service: &expr.ServiceExpr{ - ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, // Same as API - ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - }, - method: &expr.MethodExpr{ - ServerInterceptors: []*expr.InterceptorExpr{apiInterceptor}, // Same as API - ClientInterceptors: []*expr.InterceptorExpr{apiInterceptor}, - }, - expectedServer: []*expr.InterceptorExpr{apiInterceptor}, // Only one copy - expectedClient: []*expr.InterceptorExpr{apiInterceptor}, - }, + {"no-interceptors", testdata.NoInterceptorsDSL, ""}, + {"valid-interceptors", testdata.ValidInterceptorsDSL, ""}, + {"duplicate-interceptors", testdata.DuplicateInterceptorsDSL, ""}, // Duplicates are handled by merging + {"mixed-interceptors", testdata.MixedInterceptorsDSL, ""}, + {"undefined-interceptor", testdata.UndefinedInterceptorDSL, + `ServerInterceptor: interceptor "undefined" not found in service "Service" method "Method"`}, + {"empty-interceptor-name", testdata.EmptyInterceptorNameDSL, + `ServerInterceptor: interceptor name cannot be empty`}, } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - expr.Root.API = tc.api - tc.method.Service = tc.service - tc.method.Prepare() - - assert.Equal(t, tc.expectedServer, tc.method.ServerInterceptors) - assert.Equal(t, tc.expectedClient, tc.method.ClientInterceptors) - assert.NotNil(t, tc.method.Payload) - assert.NotNil(t, tc.method.StreamingPayload) - assert.NotNil(t, tc.method.Result) + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + if c.Error == "" { + expr.RunDSL(t, c.DSL) + return + } + err := expr.RunInvalidDSL(t, c.DSL) + assert.Contains(t, err.Error(), c.Error) }) } } diff --git a/expr/testdata/interceptors_validate_dsls.go b/expr/testdata/interceptors_validate_dsls.go new file mode 100644 index 0000000000..32d1d99d76 --- /dev/null +++ b/expr/testdata/interceptors_validate_dsls.go @@ -0,0 +1,83 @@ +package testdata + +import . "goa.design/goa/v3/dsl" + +var NoInterceptorsDSL = func() { + Service("Service", func() { + Method("Method", func() { + HTTP(func() { + GET("/") + }) + }) + }) +} + +var ValidInterceptorsDSL = func() { + Interceptor("api", func() {}) + API("API", func() { + ServerInterceptor("api") + }) + + Service("Service", func() { + Interceptor("service", func() {}) + ServerInterceptor("service") + + Method("Method", func() { + Interceptor("method", func() {}) + ServerInterceptor("method") + }) + }) +} + +var DuplicateInterceptorsDSL = func() { + Interceptor("duplicate", func() {}) + API("API", func() { + ServerInterceptor("duplicate") + }) + + Service("Service", func() { + ServerInterceptor("duplicate") + Method("Method", func() { + ServerInterceptor("duplicate") + }) + }) +} + +var MixedInterceptorsDSL = func() { + Interceptor("api", func() {}) + Interceptor("api-client", func() {}) + API("API", func() { + ServerInterceptor("api") + ClientInterceptor("api-client") + }) + + Service("Service", func() { + Interceptor("service", func() {}) + Interceptor("service-client", func() {}) + ServerInterceptor("service") + ClientInterceptor("service-client") + + Method("Method", func() { + Interceptor("method", func() {}) + Interceptor("method-client", func() {}) + ServerInterceptor("method") + ClientInterceptor("method-client") + }) + }) +} + +var UndefinedInterceptorDSL = func() { + Service("Service", func() { + Method("Method", func() { + ServerInterceptor("undefined") + }) + }) +} + +var EmptyInterceptorNameDSL = func() { + Service("Service", func() { + Method("Method", func() { + ServerInterceptor("") + }) + }) +} diff --git a/grpc/codegen/client.go b/grpc/codegen/client.go index fd77174a32..f6669772f2 100644 --- a/grpc/codegen/client.go +++ b/grpc/codegen/client.go @@ -65,7 +65,7 @@ func client(genpkg string, svc *expr.GRPCServiceExpr) *codegen.File { } } sections = append(sections, &codegen.SectionTemplate{ - Name: "client-init", + Name: "grpc-client-init", Source: readTemplate("client_init"), Data: data, }) diff --git a/grpc/codegen/client_cli.go b/grpc/codegen/client_cli.go index 275828a953..1135455c3c 100644 --- a/grpc/codegen/client_cli.go +++ b/grpc/codegen/client_cli.go @@ -80,6 +80,13 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da Name: svcName + pbPkgName, }) specs = append(specs, sd.Service.UserTypeImports...) + // Add interceptors import if service has client interceptors + if len(sd.Service.ClientInterceptors) > 0 { + specs = append(specs, &codegen.ImportSpec{ + Path: genpkg + "/" + sd.Service.PathName, + Name: sd.Service.PkgName, + }) + } } sections := []*codegen.SectionTemplate{ @@ -87,7 +94,7 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da cli.UsageCommands(data), cli.UsageExamples(data), { - Name: "parse-endpoint", + Name: "parse-endpoint-grpc", Source: readTemplate("parse_endpoint"), Data: struct { FlagsCode string diff --git a/grpc/codegen/parse_endpoint_test.go b/grpc/codegen/parse_endpoint_test.go new file mode 100644 index 0000000000..3a459e4b97 --- /dev/null +++ b/grpc/codegen/parse_endpoint_test.go @@ -0,0 +1,40 @@ +package codegen + +import ( + "bytes" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "goa.design/goa/v3/codegen" + "goa.design/goa/v3/expr" + "goa.design/goa/v3/grpc/codegen/testdata" +) + +func TestParseEndpointWithInterceptors(t *testing.T) { + cases := []struct { + Name string + DSL func() + }{ + { + Name: "endpoint-with-interceptors", + DSL: testdata.InterceptorsDSL, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + RunGRPCDSL(t, c.DSL) + fs := ClientCLIFiles("", expr.Root) + require.Greater(t, len(fs), 1, "expected at least 2 files") + require.NotEmpty(t, fs[0].SectionTemplates) + var buf bytes.Buffer + for _, s := range fs[0].SectionTemplates { + require.NoError(t, s.Write(&buf)) + } + code := codegen.FormatTestCode(t, buf.String()) + golden := filepath.Join("testdata", "endpoint-"+c.Name+".golden") + compareOrUpdateGolden(t, code, golden) + }) + } +} diff --git a/grpc/codegen/templates/parse_endpoint.go.tpl b/grpc/codegen/templates/parse_endpoint.go.tpl index 3cf69cde5a..43f98aec9f 100644 --- a/grpc/codegen/templates/parse_endpoint.go.tpl +++ b/grpc/codegen/templates/parse_endpoint.go.tpl @@ -2,9 +2,11 @@ // line. func ParseEndpoint( cc *grpc.ClientConn, +{{- range .Commands }} {{- if .Interceptors }} {{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors, {{- end }} +{{- end }} opts ...grpc.CallOption, ) (goa.Endpoint, any, error) { {{ .FlagsCode }} diff --git a/grpc/codegen/testdata/client-interceptors.golden b/grpc/codegen/testdata/client-interceptors.golden index f21f374e1e..44dadcce53 100644 --- a/grpc/codegen/testdata/client-interceptors.golden +++ b/grpc/codegen/testdata/client-interceptors.golden @@ -1,6 +1,6 @@ import ( "fmt" - cli "grpc/cli/test_api" + cli "grpc/cli/test" "os" "./interceptors" diff --git a/grpc/codegen/testdata/dsls.go b/grpc/codegen/testdata/dsls.go index 743ce71cb8..8bdd24b8e6 100644 --- a/grpc/codegen/testdata/dsls.go +++ b/grpc/codegen/testdata/dsls.go @@ -1018,6 +1018,13 @@ var InterceptorsDSL = func() { Description("Collects metrics for the operation") }) + API("TestAPI", func() { + Title("Test API") + Server("Test", func() { + Description("Test server") + }) + }) + Service("ServiceWithInterceptors", func() { ClientInterceptor(LogInterceptor) Method("MethodA", func() { diff --git a/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden b/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden new file mode 100644 index 0000000000..4899bed507 --- /dev/null +++ b/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden @@ -0,0 +1,178 @@ +// Test gRPC client CLI support package +// +// Command: +// goa + +package cli + +import ( + servicewithinterceptors "/service_with_interceptors" + "flag" + "fmt" + servicewithinterceptorsc "grpc/service_with_interceptors/client" + "os" + + goa "goa.design/goa/v3/pkg" + grpc "google.golang.org/grpc" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `service-with-interceptors (method-a|method-b) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` service-with-interceptors method-a --message '{ + "field": "Sapiente est." + }'` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint( + cc *grpc.ClientConn, + inter servicewithinterceptors.ClientInterceptors, + opts ...grpc.CallOption, +) (goa.Endpoint, any, error) { + var ( + serviceWithInterceptorsFlags = flag.NewFlagSet("service-with-interceptors", flag.ContinueOnError) + + serviceWithInterceptorsMethodAFlags = flag.NewFlagSet("method-a", flag.ExitOnError) + serviceWithInterceptorsMethodAMessageFlag = serviceWithInterceptorsMethodAFlags.String("message", "", "") + + serviceWithInterceptorsMethodBFlags = flag.NewFlagSet("method-b", flag.ExitOnError) + serviceWithInterceptorsMethodBMessageFlag = serviceWithInterceptorsMethodBFlags.String("message", "", "") + ) + serviceWithInterceptorsFlags.Usage = serviceWithInterceptorsUsage + serviceWithInterceptorsMethodAFlags.Usage = serviceWithInterceptorsMethodAUsage + serviceWithInterceptorsMethodBFlags.Usage = serviceWithInterceptorsMethodBUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "service-with-interceptors": + svcf = serviceWithInterceptorsFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "service-with-interceptors": + switch epn { + case "method-a": + epf = serviceWithInterceptorsMethodAFlags + + case "method-b": + epf = serviceWithInterceptorsMethodBFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "service-with-interceptors": + c := servicewithinterceptorsc.NewClient(cc, opts...) + switch epn { + + case "method-a": + endpoint = c.MethodA() + endpoint = servicewithinterceptors.WrapMethodAClientEndpoint(endpoint, inter) + data, err = servicewithinterceptorsc.BuildMethodAPayload(*serviceWithInterceptorsMethodAMessageFlag) + case "method-b": + endpoint = c.MethodB() + endpoint = servicewithinterceptors.WrapMethodBClientEndpoint(endpoint, inter) + data, err = servicewithinterceptorsc.BuildMethodBPayload(*serviceWithInterceptorsMethodBMessageFlag) + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} + +// serviceWithInterceptorsUsage displays the usage of the +// service-with-interceptors command and its subcommands. +func serviceWithInterceptorsUsage() { + fmt.Fprintf(os.Stderr, `Service is the ServiceWithInterceptors service interface. +Usage: + %[1]s [globalflags] service-with-interceptors COMMAND [flags] + +COMMAND: + method-a: MethodA implements MethodA. + method-b: MethodB implements MethodB. + +Additional help: + %[1]s service-with-interceptors COMMAND --help +`, os.Args[0]) +} +func serviceWithInterceptorsMethodAUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] service-with-interceptors method-a -message JSON + +MethodA implements MethodA. + -message JSON: + +Example: + %[1]s service-with-interceptors method-a --message '{ + "field": "Sapiente est." + }' +`, os.Args[0]) +} + +func serviceWithInterceptorsMethodBUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] service-with-interceptors method-b -message JSON + +MethodB implements MethodB. + -message JSON: + +Example: + %[1]s service-with-interceptors method-b --message '{ + "field": 3141052981090487272 + }' +`, os.Args[0]) +} diff --git a/http/codegen/client.go b/http/codegen/client.go index 52e0248712..5d70b2ed8d 100644 --- a/http/codegen/client.go +++ b/http/codegen/client.go @@ -67,7 +67,7 @@ func clientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File { } sections = append(sections, &codegen.SectionTemplate{ - Name: "client-init", + Name: "http-client-init", Source: readTemplate("client_init"), Data: data, FuncMap: map[string]any{"hasWebSocket": hasWebSocket}, From b9cab9e12a4b0c1d38a3d42130b90c21efd996b2 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Sun, 19 Jan 2025 11:33:22 -0800 Subject: [PATCH 6/6] Fix tests --- codegen/service/service_data.go | 89 ++++++++++--------- .../client_interceptor_wrappers.go.tpl | 2 +- .../templates/interceptors_types.go.tpl | 2 +- .../server_interceptor_wrappers.go.tpl | 2 +- .../chained_interceptor_service_client.golden | 20 ++--- .../chained_interceptor_service_server.golden | 20 ++--- ...read-payload_client_interceptors.go.golden | 2 +- ...ead-payload_interceptor_wrappers.go.golden | 15 ++-- ...ead-payload_service_interceptors.go.golden | 28 +++--- ...-read-result_client_interceptors.go.golden | 2 +- ...read-result_interceptor_wrappers.go.golden | 14 +-- ...read-result_service_interceptors.go.golden | 27 +++--- ...rite-payload_client_interceptors.go.golden | 2 +- ...ite-payload_interceptor_wrappers.go.golden | 15 ++-- ...ite-payload_service_interceptors.go.golden | 30 ++++--- ...write-result_client_interceptors.go.golden | 2 +- ...rite-result_interceptor_wrappers.go.golden | 14 +-- ...rite-result_service_interceptors.go.golden | 29 +++--- ...rite-payload_client_interceptors.go.golden | 2 +- ...ite-payload_interceptor_wrappers.go.golden | 15 ++-- ...ite-payload_service_interceptors.go.golden | 28 +++--- ...write-result_client_interceptors.go.golden | 2 +- ...rite-result_interceptor_wrappers.go.golden | 14 +-- ...rite-result_service_interceptors.go.golden | 27 +++--- ...interceptors_client_interceptors.go.golden | 5 +- ...nterceptors_interceptor_wrappers.go.golden | 26 +++--- ...nterceptors_service_interceptors.go.golden | 5 +- ...interceptor_interceptor_wrappers.go.golden | 20 ++++- ...interceptor_service_interceptors.go.golden | 5 +- ...-interceptor_client_interceptors.go.golden | 3 +- ...interceptor_interceptor_wrappers.go.golden | 8 +- ...interceptor_interceptor_wrappers.go.golden | 9 +- ...interceptor_service_interceptors.go.golden | 3 +- ...interceptor_interceptor_wrappers.go.golden | 20 ++++- ...interceptor_service_interceptors.go.golden | 5 +- ...ead-payload_interceptor_wrappers.go.golden | 11 +-- ...ead-payload_service_interceptors.go.golden | 28 +++--- ...read-result_interceptor_wrappers.go.golden | 11 +-- ...read-result_service_interceptors.go.golden | 27 +++--- ...nterceptors_interceptor_wrappers.go.golden | 11 +-- ...nterceptors_service_interceptors.go.golden | 3 +- 41 files changed, 339 insertions(+), 264 deletions(-) diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go index 59d7886ea9..7270e1e49b 100644 --- a/codegen/service/service_data.go +++ b/codegen/service/service_data.go @@ -280,8 +280,6 @@ type ( // MethodInterceptorData contains the data required to render the // method-level interceptor code. MethodInterceptorData struct { - // Name is the name of the interceptor. - Name string // MethodName is the name of the method. MethodName string // PayloadAccess is the name of the payload access struct. @@ -1203,47 +1201,48 @@ func buildInterceptorData(svc *expr.ServiceExpr, methods []*MethodData, i *expr. DesignName: i.Name, Description: i.Description, } - if len(svc.Methods) > 0 { - payload, result := svc.Methods[0].Payload, svc.Methods[0].Result - data.ReadPayload = collectAttributes(i.ReadPayload, payload, scope) - data.WritePayload = collectAttributes(i.WritePayload, payload, scope) - data.ReadResult = collectAttributes(i.ReadResult, result, scope) - data.WriteResult = collectAttributes(i.WriteResult, result, scope) - if len(data.ReadPayload) > 0 || len(data.WritePayload) > 0 { - data.HasPayloadAccess = true + if len(svc.Methods) == 0 { + return data + } + payload, result := svc.Methods[0].Payload, svc.Methods[0].Result + data.ReadPayload = collectAttributes(i.ReadPayload, payload, scope) + data.WritePayload = collectAttributes(i.WritePayload, payload, scope) + data.ReadResult = collectAttributes(i.ReadResult, result, scope) + data.WriteResult = collectAttributes(i.WriteResult, result, scope) + if len(data.ReadPayload) > 0 || len(data.WritePayload) > 0 { + data.HasPayloadAccess = true + } + if len(data.ReadResult) > 0 || len(data.WriteResult) > 0 { + data.HasResultAccess = true + } + for _, m := range svc.Methods { + applies := false + intExprs := m.ServerInterceptors + if !server { + intExprs = m.ClientInterceptors + } + for _, in := range intExprs { + if in.Name == i.Name { + applies = true + break + } } - if len(data.ReadResult) > 0 || len(data.WriteResult) > 0 { - data.HasResultAccess = true + if !applies { + continue } - for _, m := range svc.Methods { - applies := false - intExprs := m.ServerInterceptors - if !server { - intExprs = m.ClientInterceptors - } - for _, in := range intExprs { - if in.Name == i.Name { - applies = true - break - } - } - if !applies { - continue - } - var md *MethodData - for _, mt := range methods { - if m.Name == mt.Name { - md = mt - break - } - } - data.Methods = append(data.Methods, buildInterceptorMethodData(i, md)) - if server { - md.ServerInterceptors = append(md.ServerInterceptors, i.Name) - } else { - md.ClientInterceptors = append(md.ClientInterceptors, i.Name) + var md *MethodData + for _, mt := range methods { + if m.Name == mt.Name { + md = mt + break } } + data.Methods = append(data.Methods, buildInterceptorMethodData(i, md)) + if server { + md.ServerInterceptors = append(md.ServerInterceptors, i.Name) + } else { + md.ClientInterceptors = append(md.ClientInterceptors, i.Name) + } } return data } @@ -1258,11 +1257,17 @@ func buildInterceptorMethodData(i *expr.InterceptorExpr, md *MethodData) *Method if md.ClientStream != nil { clientStream = md.ClientStream.VarName } + var payloadAccess, resultAccess string + if i.ReadPayload != nil || i.WritePayload != nil { + payloadAccess = codegen.Goify(i.Name, false) + md.VarName + "Payload" + } + if i.ReadResult != nil || i.WriteResult != nil { + resultAccess = codegen.Goify(i.Name, false) + md.VarName + "Result" + } return &MethodInterceptorData{ - Name: i.Name, MethodName: md.VarName, - PayloadAccess: codegen.Goify(i.Name, false) + md.VarName + "Payload", - ResultAccess: codegen.Goify(i.Name, false) + md.VarName + "Result", + PayloadAccess: payloadAccess, + ResultAccess: resultAccess, PayloadRef: md.PayloadRef, ResultRef: md.ResultRef, ClientStreamInputStruct: clientStream, diff --git a/codegen/service/templates/client_interceptor_wrappers.go.tpl b/codegen/service/templates/client_interceptor_wrappers.go.tpl index 5cd5f9d9d1..cc0ff52a7b 100644 --- a/codegen/service/templates/client_interceptor_wrappers.go.tpl +++ b/codegen/service/templates/client_interceptor_wrappers.go.tpl @@ -15,7 +15,7 @@ func wrapClient{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i RawPayload: req, {{- end }} } - return i.{{ .Name }}(ctx, info, endpoint) + return i.{{ $interceptor.Name }}(ctx, info, endpoint) } } {{ end }} diff --git a/codegen/service/templates/interceptors_types.go.tpl b/codegen/service/templates/interceptors_types.go.tpl index 73a159d170..0c542b089e 100644 --- a/codegen/service/templates/interceptors_types.go.tpl +++ b/codegen/service/templates/interceptors_types.go.tpl @@ -59,4 +59,4 @@ type ( {{- end }} {{- end }} ) -{{- end }} \ No newline at end of file +{{- end }} diff --git a/codegen/service/templates/server_interceptor_wrappers.go.tpl b/codegen/service/templates/server_interceptor_wrappers.go.tpl index 3086dae3cf..cb0f5d9c44 100644 --- a/codegen/service/templates/server_interceptor_wrappers.go.tpl +++ b/codegen/service/templates/server_interceptor_wrappers.go.tpl @@ -15,7 +15,7 @@ func wrap{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i Serve RawPayload: req, {{- end }} } - return i.{{ .Name }}(ctx, info, endpoint) + return i.{{ $interceptor.Name }}(ctx, info, endpoint) } } {{- end }} diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden index bb1b3811fb..4a1eeebce9 100644 --- a/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden +++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden @@ -23,6 +23,16 @@ type ChainedInterceptorServiceClientInterceptors struct { func NewChainedInterceptorServiceClientInterceptors() *ChainedInterceptorServiceClientInterceptors { return &ChainedInterceptorServiceClientInterceptors{} } +func (i *ChainedInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Sending request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Received response: %v", resp) + return resp, nil +} func (i *ChainedInterceptorServiceClientInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) { log.Printf(ctx, "[Method] Sending request: %v", info.RawPayload) resp, err := next(ctx, info.RawPayload) @@ -43,13 +53,3 @@ func (i *ChainedInterceptorServiceClientInterceptors) Service(ctx context.Contex log.Printf(ctx, "[Service] Received response: %v", resp) return resp, nil } -func (i *ChainedInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { - log.Printf(ctx, "[API] Sending request: %v", info.RawPayload) - resp, err := next(ctx, info.RawPayload) - if err != nil { - log.Printf(ctx, "[API] Error: %v", err) - return nil, err - } - log.Printf(ctx, "[API] Received response: %v", resp) - return resp, nil -} diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden index b6cb4245f7..4027c3a597 100644 --- a/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden +++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden @@ -23,6 +23,16 @@ type ChainedInterceptorServiceServerInterceptors struct { func NewChainedInterceptorServiceServerInterceptors() *ChainedInterceptorServiceServerInterceptors { return &ChainedInterceptorServiceServerInterceptors{} } +func (i *ChainedInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { + log.Printf(ctx, "[API] Processing request: %v", info.RawPayload) + resp, err := next(ctx, info.RawPayload) + if err != nil { + log.Printf(ctx, "[API] Error: %v", err) + return nil, err + } + log.Printf(ctx, "[API] Response: %v", resp) + return resp, nil +} func (i *ChainedInterceptorServiceServerInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) { log.Printf(ctx, "[Method] Processing request: %v", info.RawPayload) resp, err := next(ctx, info.RawPayload) @@ -43,13 +53,3 @@ func (i *ChainedInterceptorServiceServerInterceptors) Service(ctx context.Contex log.Printf(ctx, "[Service] Response: %v", resp) return resp, nil } -func (i *ChainedInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) { - log.Printf(ctx, "[API] Processing request: %v", info.RawPayload) - resp, err := next(ctx, info.RawPayload) - if err != nil { - log.Printf(ctx, "[API] Error: %v", err) - return nil, err - } - log.Printf(ctx, "[API] Response: %v", resp) - return resp, nil -} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden index f1a4d2805c..5b21e6d339 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientValidation(endpoint, i, "Method") + endpoint = wrapClientMethodvalidation(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden index ffbd9bf910..1f4ac6ebd3 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapValidation applies the validation interceptor to endpoints. -func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapValidationMethod applies the validation server interceptor to endpoints. +func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithReadPayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,17 @@ func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) } } -// wrapClientValidation applies the validation interceptor to endpoints. -func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientValidationMethod applies the validation client interceptor to +// endpoints. +func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithReadPayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Validation(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden index e3edffbad9..7e616c9438 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden @@ -12,35 +12,37 @@ type ( // It includes service name, method name, and access to the endpoint. ValidationInfo goa.InterceptorInfo - // ValidationPayloadAccess provides type-safe access to the method payload. + // ValidationPayload provides type-safe access to the method payload. // It allows reading and writing specific fields of the payload as defined // in the design. - ValidationPayloadAccess interface { + ValidationPayload interface { Name() string } ) // Private implementation types type ( - validationPayloadAccess struct { + validationMethodPayload struct { payload *MethodPayload } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodvalidation(endpoint, i) + return endpoint +} + // Public accessor methods for Info types + // Payload returns a type-safe accessor for the method payload. -func (info *ValidationInfo) Payload() ValidationPayloadAccess { - return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +func (info *ValidationInfo) Payload() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} } // Private implementation methods -func (p *validationPayloadAccess) Name() string { - return p.payload.Name -} -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapValidation(endpoint, i, "Method") - return endpoint +func (p *validationMethodPayload) Name() string { + return p.payload.Name } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden index 76648aff03..7bf0271866 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientCaching(endpoint, i, "Method") + endpoint = wrapClientMethodcaching(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden index c4ad755b66..5f86fe8455 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapCaching applies the caching interceptor to endpoints. -func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapCachingMethod applies the caching server interceptor to endpoints. +func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithReadResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,16 @@ func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa } } -// wrapClientCaching applies the caching interceptor to endpoints. -func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientCachingMethod applies the caching client interceptor to endpoints. +func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithReadResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Caching(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden index de70b2f1b3..a658f22d37 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden @@ -12,35 +12,36 @@ type ( // It includes service name, method name, and access to the endpoint. CachingInfo goa.InterceptorInfo - // CachingResultAccess provides type-safe access to the method result. + // CachingResult provides type-safe access to the method result. // It allows reading and writing specific fields of the result as defined // in the design. - CachingResultAccess interface { + CachingResult interface { Data() string } ) // Private implementation types type ( - cachingResultAccess struct { + cachingMethodResult struct { result *MethodResult } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodcaching(endpoint, i) + return endpoint +} + // Public accessor methods for Info types // Result returns a type-safe accessor for the method result. -func (info *CachingInfo) Result(res any) CachingResultAccess { - return &cachingResultAccess{result: res.(*MethodResult)} +func (info *CachingInfo) Result(res any) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} } // Private implementation methods -func (r *cachingResultAccess) Data() string { - return r.result.Data -} -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapCaching(endpoint, i, "Method") - return endpoint +func (r *cachingMethodResult) Data() string { + return r.result.Data } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden index f1a4d2805c..5b21e6d339 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientValidation(endpoint, i, "Method") + endpoint = wrapClientMethodvalidation(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden index c9947a357a..f628ed5e05 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapValidation applies the validation interceptor to endpoints. -func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapValidationMethod applies the validation server interceptor to endpoints. +func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithReadWritePayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,17 @@ func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) } } -// wrapClientValidation applies the validation interceptor to endpoints. -func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientValidationMethod applies the validation client interceptor to +// endpoints. +func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithReadWritePayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Validation(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden index f4242b3d97..0f84e22320 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden @@ -12,10 +12,10 @@ type ( // It includes service name, method name, and access to the endpoint. ValidationInfo goa.InterceptorInfo - // ValidationPayloadAccess provides type-safe access to the method payload. + // ValidationPayload provides type-safe access to the method payload. // It allows reading and writing specific fields of the payload as defined // in the design. - ValidationPayloadAccess interface { + ValidationPayload interface { Name() string SetName(string) } @@ -23,28 +23,30 @@ type ( // Private implementation types type ( - validationPayloadAccess struct { + validationMethodPayload struct { payload *MethodPayload } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodvalidation(endpoint, i) + return endpoint +} + // Public accessor methods for Info types + // Payload returns a type-safe accessor for the method payload. -func (info *ValidationInfo) Payload() ValidationPayloadAccess { - return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +func (info *ValidationInfo) Payload() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} } // Private implementation methods -func (p *validationPayloadAccess) Name() string { + +func (p *validationMethodPayload) Name() string { return p.payload.Name } -func (p *validationPayloadAccess) SetName(v string) { +func (p *validationMethodPayload) SetName(v string) { p.payload.Name = v } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapValidation(endpoint, i, "Method") - return endpoint -} diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden index 76648aff03..7bf0271866 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientCaching(endpoint, i, "Method") + endpoint = wrapClientMethodcaching(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden index 65bbf27b56..1e5ab9536e 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapCaching applies the caching interceptor to endpoints. -func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapCachingMethod applies the caching server interceptor to endpoints. +func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithReadWriteResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,16 @@ func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa } } -// wrapClientCaching applies the caching interceptor to endpoints. -func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientCachingMethod applies the caching client interceptor to endpoints. +func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithReadWriteResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Caching(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden index ef8f84f67f..96c934c24e 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden @@ -12,10 +12,10 @@ type ( // It includes service name, method name, and access to the endpoint. CachingInfo goa.InterceptorInfo - // CachingResultAccess provides type-safe access to the method result. + // CachingResult provides type-safe access to the method result. // It allows reading and writing specific fields of the result as defined // in the design. - CachingResultAccess interface { + CachingResult interface { Data() string SetData(string) } @@ -23,28 +23,29 @@ type ( // Private implementation types type ( - cachingResultAccess struct { + cachingMethodResult struct { result *MethodResult } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodcaching(endpoint, i) + return endpoint +} + // Public accessor methods for Info types // Result returns a type-safe accessor for the method result. -func (info *CachingInfo) Result(res any) CachingResultAccess { - return &cachingResultAccess{result: res.(*MethodResult)} +func (info *CachingInfo) Result(res any) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} } // Private implementation methods -func (r *cachingResultAccess) Data() string { + +func (r *cachingMethodResult) Data() string { return r.result.Data } -func (r *cachingResultAccess) SetData(v string) { +func (r *cachingMethodResult) SetData(v string) { r.result.Data = v } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapCaching(endpoint, i, "Method") - return endpoint -} diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden index f1a4d2805c..5b21e6d339 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientValidation(endpoint, i, "Method") + endpoint = wrapClientMethodvalidation(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden index 34e9a283bd..70657d6eef 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapValidation applies the validation interceptor to endpoints. -func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapValidationMethod applies the validation server interceptor to endpoints. +func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithWritePayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,17 @@ func wrapValidation(endpoint goa.Endpoint, i ServerInterceptors, method string) } } -// wrapClientValidation applies the validation interceptor to endpoints. -func wrapClientValidation(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientValidationMethod applies the validation client interceptor to +// endpoints. +func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &ValidationInfo{ Service: "InterceptorWithWritePayload", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Validation(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden index e54173d388..c76eb125e0 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden @@ -12,35 +12,37 @@ type ( // It includes service name, method name, and access to the endpoint. ValidationInfo goa.InterceptorInfo - // ValidationPayloadAccess provides type-safe access to the method payload. + // ValidationPayload provides type-safe access to the method payload. // It allows reading and writing specific fields of the payload as defined // in the design. - ValidationPayloadAccess interface { + ValidationPayload interface { SetName(string) } ) // Private implementation types type ( - validationPayloadAccess struct { + validationMethodPayload struct { payload *MethodPayload } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodvalidation(endpoint, i) + return endpoint +} + // Public accessor methods for Info types + // Payload returns a type-safe accessor for the method payload. -func (info *ValidationInfo) Payload() ValidationPayloadAccess { - return &validationPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +func (info *ValidationInfo) Payload() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} } // Private implementation methods -func (p *validationPayloadAccess) SetName(v string) { - p.payload.Name = v -} -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapValidation(endpoint, i, "Method") - return endpoint +func (p *validationMethodPayload) SetName(v string) { + p.payload.Name = v } diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden index 76648aff03..7bf0271866 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden @@ -9,6 +9,6 @@ type ClientInterceptors interface { // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientCaching(endpoint, i, "Method") + endpoint = wrapClientMethodcaching(endpoint, i) return endpoint } diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden index fad8d4bbe7..bfcca2ab0c 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapCaching applies the caching interceptor to endpoints. -func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapCachingMethod applies the caching server interceptor to endpoints. +func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithWriteResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,15 +13,16 @@ func wrapCaching(endpoint goa.Endpoint, i ServerInterceptors, method string) goa } } -// wrapClientCaching applies the caching interceptor to endpoints. -func wrapClientCaching(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientCachingMethod applies the caching client interceptor to endpoints. +func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &CachingInfo{ Service: "InterceptorWithWriteResult", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Caching(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden index 1efc553c96..26b2620a97 100644 --- a/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden @@ -12,35 +12,36 @@ type ( // It includes service name, method name, and access to the endpoint. CachingInfo goa.InterceptorInfo - // CachingResultAccess provides type-safe access to the method result. + // CachingResult provides type-safe access to the method result. // It allows reading and writing specific fields of the result as defined // in the design. - CachingResultAccess interface { + CachingResult interface { SetData(string) } ) // Private implementation types type ( - cachingResultAccess struct { + cachingMethodResult struct { result *MethodResult } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodcaching(endpoint, i) + return endpoint +} + // Public accessor methods for Info types // Result returns a type-safe accessor for the method result. -func (info *CachingInfo) Result(res any) CachingResultAccess { - return &cachingResultAccess{result: res.(*MethodResult)} +func (info *CachingInfo) Result(res any) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} } // Private implementation methods -func (r *cachingResultAccess) SetData(v string) { - r.result.Data = v -} -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapCaching(endpoint, i, "Method") - return endpoint +func (r *cachingMethodResult) SetData(v string) { + r.result.Data = v } diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden index b0f78bbbb5..00d3b74592 100644 --- a/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden @@ -20,7 +20,8 @@ type ( // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientTest2(endpoint, i, "Method") - endpoint = wrapClientTest4(endpoint, i, "Method") + endpoint = wrapClientMethodtest2(endpoint, i) + endpoint = wrapClientMethodtest4(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden index dd2afccd42..f23c4d7171 100644 --- a/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden @@ -1,10 +1,11 @@ -// wrapTest applies the test interceptor to endpoints. -func wrapTest(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapTestMethod applies the test server interceptor to endpoints. +func wrapMethodTest(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &TestInfo{ Service: "MultipleInterceptorsService", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -12,12 +13,12 @@ func wrapTest(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.En } } -// wrapTest3 applies the test3 interceptor to endpoints. -func wrapTest3(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { +// wrapTest3Method applies the test3 server interceptor to endpoints. +func wrapMethodTest3(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &Test3Info{ Service: "MultipleInterceptorsService", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -25,12 +26,12 @@ func wrapTest3(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.E } } -// wrapClientTest2 applies the test2 interceptor to endpoints. -func wrapClientTest2(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientTest2Method applies the test2 client interceptor to endpoints. +func wrapClientMethodTest2(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &Test2Info{ Service: "MultipleInterceptorsService", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } @@ -38,15 +39,16 @@ func wrapClientTest2(endpoint goa.Endpoint, i ClientInterceptors, method string) } } -// wrapClientTest4 applies the test4 interceptor to endpoints. -func wrapClientTest4(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { +// wrapClientTest4Method applies the test4 client interceptor to endpoints. +func wrapClientMethodTest4(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &Test4Info{ Service: "MultipleInterceptorsService", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Test4(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden index 7d8c68e506..1df6e00cf8 100644 --- a/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden @@ -20,7 +20,8 @@ type ( // WrapMethodEndpoint wraps the Method endpoint with the server-side // interceptors defined in the design. func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapTest(endpoint, i, "Method") - endpoint = wrapTest3(endpoint, i, "Method") + endpoint = wrapMethodtest(endpoint, i) + endpoint = wrapMethodtest3(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden index 1decfd7599..b7e0eebf4c 100644 --- a/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden @@ -1,13 +1,27 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "SingleAPIServerInterceptor", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Logging(ctx, info, endpoint) } } + +// wrapLoggingMethod2 applies the logging server interceptor to endpoints. +func wrapMethod2Logging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "SingleAPIServerInterceptor", + Method: "Method2", + Endpoint: endpoint, + RawPayload: req, + } + return i.Logging(ctx, info, endpoint) + } +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden index a5af7d39d5..f4870a7eef 100644 --- a/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden @@ -16,13 +16,14 @@ type ( // WrapMethodEndpoint wraps the Method endpoint with the server-side // interceptors defined in the design. func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") + endpoint = wrapMethodlogging(endpoint, i) return endpoint } // WrapMethod2Endpoint wraps the Method2 endpoint with the server-side // interceptors defined in the design. func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method2") + endpoint = wrapMethod2logging(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden index 41f9b16414..8f272d85e6 100644 --- a/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden @@ -16,6 +16,7 @@ type ( // WrapMethodClientEndpoint wraps the Method endpoint with the client // interceptors defined in the design. func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { - endpoint = wrapClientTracing(endpoint, i, "Method") + endpoint = wrapClientMethodtracing(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden index 4744840adf..b05666e31d 100644 --- a/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden @@ -1,13 +1,15 @@ -// wrapClientTracing applies the tracing interceptor to endpoints. -func wrapClientTracing(endpoint goa.Endpoint, i ClientInterceptors, method string) goa.Endpoint { + +// wrapClientTracingMethod applies the tracing client interceptor to endpoints. +func wrapClientMethodTracing(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &TracingInfo{ Service: "SingleClientInterceptor", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Tracing(ctx, info, endpoint) } } + diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden index 57c9dce068..c06dbb3edd 100644 --- a/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden @@ -1,13 +1,14 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "SingleMethodServerInterceptor", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Logging(ctx, info, endpoint) } -} +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden index 05e445a7f2..58b647a34a 100644 --- a/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden @@ -16,6 +16,7 @@ type ( // WrapMethodEndpoint wraps the Method endpoint with the server-side // interceptors defined in the design. func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") + endpoint = wrapMethodlogging(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden index 6e8b74a57c..c15ab43987 100644 --- a/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden @@ -1,13 +1,27 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "SingleServerInterceptor", - Method: method, + Method: "Method", Endpoint: endpoint, RawPayload: req, } return i.Logging(ctx, info, endpoint) } } + +// wrapLoggingMethod2 applies the logging server interceptor to endpoints. +func wrapMethod2Logging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + info := &LoggingInfo{ + Service: "SingleServerInterceptor", + Method: "Method2", + Endpoint: endpoint, + RawPayload: req, + } + return i.Logging(ctx, info, endpoint) + } +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden index a5af7d39d5..f4870a7eef 100644 --- a/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden @@ -16,13 +16,14 @@ type ( // WrapMethodEndpoint wraps the Method endpoint with the server-side // interceptors defined in the design. func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") + endpoint = wrapMethodlogging(endpoint, i) return endpoint } // WrapMethod2Endpoint wraps the Method2 endpoint with the server-side // interceptors defined in the design. func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method2") + endpoint = wrapMethod2logging(endpoint, i) return endpoint } + diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden index f5a3f738df..554dade015 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden @@ -1,13 +1,14 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "StreamingInterceptorsWithReadPayload", - Method: method, + Method: "Method", Endpoint: endpoint, - RawPayload: req.(*MethodEndpointInput).Payload, + RawPayload: req.(*MethodServerStream).Payload, } return i.Logging(ctx, info, endpoint) } -} +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden index 8f6ed7f94c..4a173a3ce4 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden @@ -12,39 +12,41 @@ type ( // It includes service name, method name, and access to the endpoint. LoggingInfo goa.InterceptorInfo - // LoggingPayloadAccess provides type-safe access to the method payload. + // LoggingPayload provides type-safe access to the method payload. // It allows reading and writing specific fields of the payload as defined // in the design. - LoggingPayloadAccess interface { + LoggingPayload interface { Initial() string } ) // Private implementation types type ( - loggingPayloadAccess struct { + loggingMethodPayload struct { payload *MethodPayload } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodlogging(endpoint, i) + return endpoint +} + // Public accessor methods for Info types + // Payload returns a type-safe accessor for the method payload. -func (info *LoggingInfo) Payload() LoggingPayloadAccess { - return &loggingPayloadAccess{payload: info.RawPayload.(*MethodPayload)} +func (info *LoggingInfo) Payload() LoggingPayload { + return &loggingMethodPayload{payload: info.RawPayload.(*MethodPayload)} } // Private implementation methods -func (p *loggingPayloadAccess) Initial() string { + +func (p *loggingMethodPayload) Initial() string { if p.payload.Initial == nil { var zero string return zero } return *p.payload.Initial } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") - return endpoint -} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden index 2cb886bc02..2fc769d48b 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden @@ -1,13 +1,14 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "StreamingInterceptorsWithReadResult", - Method: method, + Method: "Method", Endpoint: endpoint, - RawPayload: req.(*MethodEndpointInput).Payload, + RawPayload: req.(*MethodServerStream).Payload, } return i.Logging(ctx, info, endpoint) } -} +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden index bc5516387c..fc61ae626f 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden @@ -12,39 +12,40 @@ type ( // It includes service name, method name, and access to the endpoint. LoggingInfo goa.InterceptorInfo - // LoggingResultAccess provides type-safe access to the method result. + // LoggingResult provides type-safe access to the method result. // It allows reading and writing specific fields of the result as defined // in the design. - LoggingResultAccess interface { + LoggingResult interface { Data() string } ) // Private implementation types type ( - loggingResultAccess struct { + loggingMethodResult struct { result *MethodResult } ) +// WrapMethodEndpoint wraps the Method endpoint with the server-side +// interceptors defined in the design. +func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { + endpoint = wrapMethodlogging(endpoint, i) + return endpoint +} + // Public accessor methods for Info types // Result returns a type-safe accessor for the method result. -func (info *LoggingInfo) Result(res any) LoggingResultAccess { - return &loggingResultAccess{result: res.(*MethodResult)} +func (info *LoggingInfo) Result(res any) LoggingResult { + return &loggingMethodResult{result: res.(*MethodResult)} } // Private implementation methods -func (r *loggingResultAccess) Data() string { + +func (r *loggingMethodResult) Data() string { if r.result.Data == nil { var zero string return zero } return *r.result.Data } - -// WrapMethodEndpoint wraps the Method endpoint with the server-side -// interceptors defined in the design. -func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") - return endpoint -} diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden index e0841bc4be..0361aa2387 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden @@ -1,13 +1,14 @@ -// wrapLogging applies the logging interceptor to endpoints. -func wrapLogging(endpoint goa.Endpoint, i ServerInterceptors, method string) goa.Endpoint { + +// wrapLoggingMethod applies the logging server interceptor to endpoints. +func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { info := &LoggingInfo{ Service: "StreamingInterceptors", - Method: method, + Method: "Method", Endpoint: endpoint, - RawPayload: req.(*MethodEndpointInput).Payload, + RawPayload: req.(*MethodServerStream).Payload, } return i.Logging(ctx, info, endpoint) } -} +} \ No newline at end of file diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden index 05e445a7f2..58b647a34a 100644 --- a/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden +++ b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden @@ -16,6 +16,7 @@ type ( // WrapMethodEndpoint wraps the Method endpoint with the server-side // interceptors defined in the design. func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint { - endpoint = wrapLogging(endpoint, i, "Method") + endpoint = wrapMethodlogging(endpoint, i) return endpoint } +