diff --git a/codegen/cli/cli.go b/codegen/cli/cli.go index a69f747036..6c852cc5e8 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, } } @@ -682,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/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..c2b09fcae3 --- /dev/null +++ b/codegen/example/templates/server_interceptors.go.tpl @@ -0,0 +1,19 @@ +{{- if mustInitServices .Services }} + {{- if .HasInterceptors }} + {{ comment "Initialize the interceptors." }} + var ( + {{- range .Services }} + {{- if and .Methods .ServerInterceptors }} + {{ .VarName }}Interceptors {{ .PkgName }}.ServerInterceptors + {{- end }} + {{- end }} + ) + { + {{- range .Services }} + {{- if and .Methods .ServerInterceptors }} + {{ .VarName }}Interceptors = {{ $.InterPkg }}.New{{ .StructName }}ServerInterceptors() + {{- end }} + {{- end }} + } + {{- 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 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/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..8ca7156704 100644 --- a/codegen/service/endpoint.go +++ b/codegen/service/endpoint.go @@ -30,6 +30,12 @@ type ( // 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. @@ -61,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 ) @@ -128,8 +134,7 @@ 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 { @@ -142,16 +147,18 @@ func endpointData(service *expr.ServiceExpr) *EndpointsData { } 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, - Description: desc, - VarName: endpointsStructName, - ClientVarName: clientStructName, - ServiceVarName: serviceInterfaceName, - ClientInitArgs: strings.Join(names, ", "), - Methods: methods, - Schemes: svc.Schemes, + Name: svc.Name, + Description: desc, + VarName: endpointsStructName, + ClientVarName: clientStructName, + ServiceVarName: serviceInterfaceName, + ClientInitArgs: strings.Join(names, ", "), + Methods: methods, + Schemes: svc.Schemes, + HasServerInterceptors: len(svc.ServerInterceptors) > 0, + HasClientInterceptors: len(svc.ClientInterceptors) > 0, } } 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/example_interceptors.go b/codegen/service/example_interceptors.go new file mode 100644 index 0000000000..7c24b08bea --- /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: "exmaple-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: "example-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 new file mode 100644 index 0000000000..8da90f9f1e --- /dev/null +++ b/codegen/service/interceptors.go @@ -0,0 +1,179 @@ +package service + +import ( + "path/filepath" + + "goa.design/goa/v3/codegen" + "goa.design/goa/v3/expr" +) + +// 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) + + // Generate service-specific interceptor files + if len(svc.ServerInterceptors) > 0 { + files = append(files, interceptorFile(svc, true)) + } + if len(svc.ClientInterceptors) > 0 { + files = append(files, interceptorFile(svc, false)) + } + + // Generate wrapper file if this service has any interceptors + if len(svc.ServerInterceptors) > 0 || len(svc.ClientInterceptors) > 0 { + files = append(files, wrapperFile(svc)) + } + + return files +} + +// 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-type" + desc := "Client Interceptors" + if server { + filename = "service_interceptors.go" + template = "server_interceptors" + section = "server-interceptors-type" + desc = "Server Interceptors" + } + desc = svc.Name + desc + path := filepath.Join(codegen.Gendir, svc.PathName, filename) + + interceptors := svc.ServerInterceptors + if !server { + 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{}{} + } + filtered := make([]*InterceptorData, 0, len(interceptors)) + for _, in := range interceptors { + if _, ok := names[in.Name]; !ok { + filtered = append(filtered, in) + } + } + interceptors = filtered + } + + sections := []*codegen.SectionTemplate{ + codegen.Header(desc, svc.PkgName, []*codegen.ImportSpec{ + {Path: "context"}, + codegen.GoaImport(""), + }), + { + Name: section, + Source: readTemplate(template), + Data: svc, + }, + } + if len(interceptors) > 0 { + sections = append(sections, &codegen.SectionTemplate{ + Name: "interceptor-types", + Source: readTemplate("interceptors_types"), + Data: interceptors, + FuncMap: map[string]any{ + "hasPrivateImplementationTypes": hasPrivateImplementationTypes, + }, + }) + } + + template = "endpoint_wrappers" + section = "endpoint-wrapper" + if !server { + template = "client_wrappers" + section = "client-wrapper" + } + for _, m := range svc.Methods { + ints := m.ServerInterceptors + if !server { + ints = m.ClientInterceptors + } + if len(ints) == 0 { + continue + } + sections = append(sections, &codegen.SectionTemplate{ + Name: section, + Source: readTemplate(template), + Data: map[string]interface{}{ + "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} +} + +// 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, + }, + }) + } + 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, + }, + }) + } + + return &codegen.File{ + Path: path, + SectionTemplates: sections, + } +} + +// 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 +} 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 new file mode 100644 index 0000000000..6fa7c3ab14 --- /dev/null +++ b/codegen/service/interceptors_test.go @@ -0,0 +1,206 @@ +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() + expectedFileCount int + }{ + {"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 := InterceptorsFiles("goa.design/goa/example", root.Services[0]) + + 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") + + golden := filepath.Join("testdata", "interceptors", c.Name+"_"+filepath.Base(f.Path)+".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", Pointer: 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", Pointer: 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", Pointer: false}, + {Name: "Age", TypeRef: "int", Pointer: 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..706ea255ad 100644 --- a/codegen/service/service.go +++ b/codegen/service/service.go @@ -195,6 +195,9 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s } files := []*codegen.File{{Path: svcPath, SectionTemplates: sections}} + // 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..7270e1e49b 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 contains the data needed to render the server-side + // interceptors code. + ServerInterceptors []*InterceptorData + // ClientInterceptors contains the data needed to render the client-side + // interceptors code. + 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 []string + // ClientInterceptors list the client interceptors that apply to this + // method. + ClientInterceptors []string // ViewedResult contains the data required to generate the code handling // views if any. ViewedResult *ViewedResultTypeData @@ -252,6 +226,94 @@ 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 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 + // Description is the description of the interceptor from the design. + Description string + // Methods + Methods []*MethodInterceptorData + // ReadPayload contains payload attributes that the interceptor can + // read. + ReadPayload []*AttributeData + // WritePayload contains payload attributes that the interceptor can + // write. + WritePayload []*AttributeData + // ReadResult contains result attributes that the interceptor can read. + ReadResult []*AttributeData + // WriteResult contains result attributes that the interceptor can + // write. + WriteResult []*AttributeData + // 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 { + // 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 if the endpoint defines a client stream. + 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 + // Pointer is true if the attribute is a pointer. + Pointer 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 +342,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" @@ -769,6 +842,8 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data { ViewsPkg: viewspkg, Methods: methods, Schemes: schemes, + ServerInterceptors: collectInterceptors(service, methods, scope, true), + ClientInterceptors: collectInterceptors(service, methods, scope, false), Scope: scope, ViewScope: viewScope, errorTypes: errTypes, @@ -779,11 +854,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 { @@ -1001,14 +1112,15 @@ 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) 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 +1194,87 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe data.StreamingPayloadEx = spayloadEx } +// 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 { + 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 !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) + } + } + 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 + } + 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{ + MethodName: md.VarName, + PayloadAccess: payloadAccess, + ResultAccess: resultAccess, + PayloadRef: md.PayloadRef, + ResultRef: md.ResultRef, + ClientStreamInputStruct: clientStream, + ServerStreamInputStruct: serverStream, + } +} + // 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 +1377,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), + Pointer: 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..cc0ff52a7b --- /dev/null +++ b/codegen/service/templates/client_interceptor_wrappers.go.tpl @@ -0,0 +1,22 @@ +{{- range .ClientInterceptors }} +{{- $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 := &{{ $interceptor.Name }}Info{ + Service: "{{ $.Service }}", + Method: "{{ .MethodName }}", + Endpoint: endpoint, + {{- if .ClientStreamInputStruct }} + RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + return i.{{ $interceptor.Name }}(ctx, info, endpoint) + } +} +{{ end }} +{{- 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 new file mode 100644 index 0000000000..9287d25b35 --- /dev/null +++ b/codegen/service/templates/client_wrappers.go.tpl @@ -0,0 +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 .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 new file mode 100644 index 0000000000..deffd14fc9 --- /dev/null +++ b/codegen/service/templates/endpoint_wrappers.go.tpl @@ -0,0 +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 .Interceptors }} + endpoint = wrap{{ $.MethodVarName }}{{ . }}(endpoint, i) + {{- end }} + return endpoint +} 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 new file mode 100644 index 0000000000..ea3b0b5e86 --- /dev/null +++ b/codegen/service/templates/interceptors.go.tpl @@ -0,0 +1,96 @@ +{{- if hasPrivateImplementationTypes . }} +// Public accessor methods for Info types +{{- range . }} + {{- if .HasPayloadAccess }} + +// Payload returns a type-safe accessor for the method payload. +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 .HasResultAccess }} +// Result returns a type-safe accessor for the method result. +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 .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 }} + return p.payload.{{ .Name }} + {{- end }} +} + {{- end }} + + {{- range $interceptor.WritePayload }} +func (p *{{ $method.PayloadAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .Pointer }} + p.payload.{{ .Name }} = &v + {{- else }} + p.payload.{{ .Name }} = v + {{- end }} +} + {{- end }} + + {{- 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 }} + return r.result.{{ .Name }} + {{- end }} +} + {{- end }} + + {{- range $interceptor.WriteResult }} +func (r *{{ $method.ResultAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) { + {{- if .Pointer }} + r.result.{{ .Name }} = &v + {{- else }} + r.result.{{ .Name }} = v + {{- 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..0c542b089e --- /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 }} 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..cb0f5d9c44 --- /dev/null +++ b/codegen/service/templates/server_interceptor_wrappers.go.tpl @@ -0,0 +1,22 @@ +{{- range .ServerInterceptors }} +{{- $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 := &{{ $interceptor.Name }}Info{ + Service: "{{ $.Service }}", + Method: "{{ .MethodName }}", + Endpoint: endpoint, + {{- if .ServerStreamInputStruct }} + RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload, + {{- else }} + RawPayload: req, + {{- end }} + } + return i.{{ $interceptor.Name }}(ctx, info, endpoint) + } +} +{{- 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 new file mode 100644 index 0000000000..cdfb9287ec --- /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/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..a13cacde3a 100644 --- a/codegen/service/testdata/client_code.go +++ b/codegen/service/testdata/client_code.go @@ -283,3 +283,28 @@ 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 +} +` diff --git a/codegen/service/testdata/endpoint_code.go b/codegen/service/testdata/endpoint_code.go index 978ff5e5f7..324d104ae8 100644 --- a/codegen/service/testdata/endpoint_code.go +++ b/codegen/service/testdata/endpoint_code.go @@ -513,3 +513,101 @@ 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) + } +} +` + +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) + } +} +` + +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) + } +} +` 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/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..4a1eeebce9 --- /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) 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) + 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 +} 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..4027c3a597 --- /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) 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) + 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 +} 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..5b21e6d339 --- /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 = 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 new file mode 100644 index 0000000000..1f4ac6ebd3 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden @@ -0,0 +1,29 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, 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", + 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 new file mode 100644 index 0000000000..7e616c9438 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden @@ -0,0 +1,48 @@ +// 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 { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (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 + + // ValidationPayload provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayload interface { + Name() string + } +) + +// Private implementation types +type ( + 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() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods + +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 new file mode 100644 index 0000000000..7bf0271866 --- /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 = 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 new file mode 100644 index 0000000000..5f86fe8455 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden @@ -0,0 +1,28 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, 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", + 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 new file mode 100644 index 0000000000..a658f22d37 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden @@ -0,0 +1,47 @@ +// 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 { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (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 + + // CachingResult provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResult interface { + Data() string + } +) + +// Private implementation types +type ( + 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) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} +} + +// Private implementation methods + +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 new file mode 100644 index 0000000000..5b21e6d339 --- /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 = 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 new file mode 100644 index 0000000000..f628ed5e05 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden @@ -0,0 +1,29 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, 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", + 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 new file mode 100644 index 0000000000..0f84e22320 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden @@ -0,0 +1,52 @@ +// 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 { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (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 + + // ValidationPayload provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayload interface { + Name() string + SetName(string) + } +) + +// Private implementation types +type ( + 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() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods + +func (p *validationMethodPayload) Name() string { + return p.payload.Name +} +func (p *validationMethodPayload) SetName(v string) { + p.payload.Name = v +} 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..7bf0271866 --- /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 = 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 new file mode 100644 index 0000000000..1e5ab9536e --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden @@ -0,0 +1,28 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, 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", + 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 new file mode 100644 index 0000000000..96c934c24e --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden @@ -0,0 +1,51 @@ +// 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 { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (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 + + // CachingResult provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResult interface { + Data() string + SetData(string) + } +) + +// Private implementation types +type ( + 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) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} +} + +// Private implementation methods + +func (r *cachingMethodResult) Data() string { + return r.result.Data +} +func (r *cachingMethodResult) SetData(v string) { + r.result.Data = v +} 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..5b21e6d339 --- /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 = 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 new file mode 100644 index 0000000000..70657d6eef --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden @@ -0,0 +1,29 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Validation(ctx, info, 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", + 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 new file mode 100644 index 0000000000..c76eb125e0 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden @@ -0,0 +1,48 @@ +// 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 { + Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (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 + + // ValidationPayload provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + ValidationPayload interface { + SetName(string) + } +) + +// Private implementation types +type ( + 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() ValidationPayload { + return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods + +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 new file mode 100644 index 0000000000..7bf0271866 --- /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 = 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 new file mode 100644 index 0000000000..bfcca2ab0c --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden @@ -0,0 +1,28 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Caching(ctx, info, 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", + 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 new file mode 100644 index 0000000000..26b2620a97 --- /dev/null +++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden @@ -0,0 +1,47 @@ +// 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 { + Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (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 + + // CachingResult provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + CachingResult interface { + SetData(string) + } +) + +// Private implementation types +type ( + 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) CachingResult { + return &cachingMethodResult{result: res.(*MethodResult)} +} + +// Private implementation methods + +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 new file mode 100644 index 0000000000..00d3b74592 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden @@ -0,0 +1,27 @@ +// 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 = 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 new file mode 100644 index 0000000000..f23c4d7171 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden @@ -0,0 +1,54 @@ + + +// 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Test(ctx, info, 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Test3(ctx, info, 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", + Endpoint: endpoint, + RawPayload: req, + } + return i.Test2(ctx, info, 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", + 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..1df6e00cf8 --- /dev/null +++ b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden @@ -0,0 +1,27 @@ +// 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 = 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 new file mode 100644 index 0000000000..b7e0eebf4c --- /dev/null +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,27 @@ + + +// 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", + 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 new file mode 100644 index 0000000000..f4870a7eef --- /dev/null +++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,29 @@ +// 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 = 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 = 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 new file mode 100644 index 0000000000..8f272d85e6 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden @@ -0,0 +1,22 @@ +// 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 = 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 new file mode 100644 index 0000000000..b05666e31d --- /dev/null +++ b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,15 @@ + + +// 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", + 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 new file mode 100644 index 0000000000..c06dbb3edd --- /dev/null +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,14 @@ + + +// 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", + 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 new file mode 100644 index 0000000000..58b647a34a --- /dev/null +++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,22 @@ +// 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 = 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 new file mode 100644 index 0000000000..c15ab43987 --- /dev/null +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden @@ -0,0 +1,27 @@ + + +// 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", + 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 new file mode 100644 index 0000000000..f4870a7eef --- /dev/null +++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden @@ -0,0 +1,29 @@ +// 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 = 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 = 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 new file mode 100644 index 0000000000..554dade015 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden @@ -0,0 +1,14 @@ + + +// 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", + Endpoint: endpoint, + 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 new file mode 100644 index 0000000000..4a173a3ce4 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden @@ -0,0 +1,52 @@ +// 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 + + // LoggingPayload provides type-safe access to the method payload. + // It allows reading and writing specific fields of the payload as defined + // in the design. + LoggingPayload interface { + Initial() string + } +) + +// Private implementation types +type ( + 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() LoggingPayload { + return &loggingMethodPayload{payload: info.RawPayload.(*MethodPayload)} +} + +// Private implementation methods + +func (p *loggingMethodPayload) 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_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..2fc769d48b --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden @@ -0,0 +1,14 @@ + + +// 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", + Endpoint: endpoint, + 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 new file mode 100644 index 0000000000..fc61ae626f --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden @@ -0,0 +1,51 @@ +// 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 + + // LoggingResult provides type-safe access to the method result. + // It allows reading and writing specific fields of the result as defined + // in the design. + LoggingResult interface { + Data() string + } +) + +// Private implementation types +type ( + 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) LoggingResult { + return &loggingMethodResult{result: res.(*MethodResult)} +} + +// Private implementation methods + +func (r *loggingMethodResult) 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_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden new file mode 100644 index 0000000000..0361aa2387 --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden @@ -0,0 +1,14 @@ + + +// 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", + Endpoint: endpoint, + 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 new file mode 100644 index 0000000000..58b647a34a --- /dev/null +++ b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden @@ -0,0 +1,22 @@ +// 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 = wrapMethodlogging(endpoint, i) + return endpoint +} + 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..fdb42fa1b8 --- /dev/null +++ b/dsl/interceptor.go @@ -0,0 +1,297 @@ +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: + 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 { + 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/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/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/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/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..0183545e0e 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} @@ -97,11 +102,22 @@ func (m *MethodExpr) Prepare() { } } -// 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 +201,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 +236,44 @@ func (m *MethodExpr) Validate() error { return verr } +// 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 bd2bef8611..6d57459665 100644 --- a/expr/method_test.go +++ b/expr/method_test.go @@ -154,3 +154,31 @@ func TestMethodExprIsPayloadStreaming(t *testing.T) { } } } + +func TestMethodExprValidateInterceptors(t *testing.T) { + cases := []struct { + Name string + DSL func() + Error string + }{ + {"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 _, 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/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/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/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/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/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/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/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..43f98aec9f 100644 --- a/grpc/codegen/templates/parse_endpoint.go.tpl +++ b/grpc/codegen/templates/parse_endpoint.go.tpl @@ -1,6 +1,14 @@ // 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, +{{- range .Commands }} + {{- if .Interceptors }} + {{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors, + {{- end }} +{{- end }} + opts ...grpc.CallOption, +) (goa.Endpoint, any, error) { {{ .FlagsCode }} var ( data any @@ -13,9 +21,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..44dadcce53 --- /dev/null +++ b/grpc/codegen/testdata/client-interceptors.golden @@ -0,0 +1,30 @@ +import ( + "fmt" + cli "grpc/cli/test" + "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..8bdd24b8e6 100644 --- a/grpc/codegen/testdata/dsls.go +++ b/grpc/codegen/testdata/dsls.go @@ -1009,3 +1009,34 @@ 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") + }) + + API("TestAPI", func() { + Title("Test API") + Server("Test", func() { + Description("Test server") + }) + }) + + 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/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}, 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/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/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 new file mode 100644 index 0000000000..c784f14b88 --- /dev/null +++ b/pkg/interceptor.go @@ -0,0 +1,17 @@ +package goa + +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 + } +)