Repository: goadesign/goa
Pull request #2703 (2020-11-01 00:00:37)

Fix duplicate generated result type definition
Ensure that result types used by multiple methods get only defined once.
Note that if the result type defined multiple views and if a method
result definition specific a view explicitly then we must generate a
different result type in that case.


Patch:
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 6729307a..7d927832 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -486,6 +486,7 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 		seenErrors map[string]struct{}
 		seen       map[string]struct{}
 		seenProj   map[string]*ProjectedTypeData
+		seenViewed map[string]*ViewedResultTypeData
 	)
 	{
 		scope = codegen.NewNameScope()
@@ -496,6 +497,7 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 		seen = make(map[string]struct{})
 		seenErrors = make(map[string]struct{})
 		seenProj = make(map[string]*ProjectedTypeData)
+		seenViewed = make(map[string]*ViewedResultTypeData)
 
 		// A function to collect user types from an error expression
 		recordError := func(er *expr.ErrorExpr) {
@@ -586,11 +588,20 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 		for i, e := range service.Methods {
 			m := buildMethodData(e, pkgName, service, scope)
 			if rt, ok := e.Result.Type.(*expr.ResultTypeExpr); ok {
-				projected := seenProj[rt.ID()]
-				projAtt := &expr.AttributeExpr{Type: projected.Type}
-				vrt := buildViewedResultType(e.Result, projAtt, viewspkg, scope, viewScope)
-				viewedRTs = append(viewedRTs, vrt)
-				m.ViewedResult = vrt
+				var view string
+				if v, ok := e.Result.Meta["view"]; ok {
+					view = v[0]
+				}
+				if vrt, ok := seenViewed[m.Result+"::"+view]; ok {
+					m.ViewedResult = vrt
+				} else {
+					projected := seenProj[rt.ID()]
+					projAtt := &expr.AttributeExpr{Type: projected.Type}
+					vrt := buildViewedResultType(e.Result, projAtt, viewspkg, scope, viewScope)
+					viewedRTs = append(viewedRTs, vrt)
+					m.ViewedResult = vrt
+					seenViewed[vrt.Name+"::"+view] = vrt
+				}
 			}
 			methods[i] = m
 			for _, s := range m.Schemes {
@@ -974,8 +985,11 @@ func BuildSchemeData(s *expr.SchemeExpr, m *expr.MethodExpr) *SchemeData {
 }
 
 // collectProjectedTypes builds a projected type for every user type found
-// when recursing through the attributes. It stores the projected types in
-// data.
+// when recursing through the attributes. The projected types live in the views
+// package and support the marshaling and unmarshalling of result types that
+// make use of views. We need to build projected types for all user types - not
+// just result types - because user types make contain result types and thus may
+// need to be marshalled in different ways depending on the view being used.
 func collectProjectedTypes(projected, att *expr.AttributeExpr, viewspkg string, scope, viewScope *codegen.NameScope, seen map[string]*ProjectedTypeData) (data []*ProjectedTypeData) {
 	collect := func(projected, att *expr.AttributeExpr) []*ProjectedTypeData {
 		return collectProjectedTypes(projected, att, viewspkg, scope, viewScope, seen)


Test patch:
diff --git a/codegen/service/testdata/views_code.go b/codegen/service/testdata/views_code.go
index 27ad51cc..a9a3f389 100644
--- a/codegen/service/testdata/views_code.go
+++ b/codegen/service/testdata/views_code.go
@@ -712,3 +712,44 @@ func ValidateAnotherResultCollectionView(result AnotherResultCollectionView) (er
 	return
 }
 `
+
+const ResultWithMultipleMethodsCode = `// RT is the viewed result type that is projected based on a view.
+type RT struct {
+	// Type to project
+	Projected *RTView
+	// View to render
+	View string
+}
+
+// RTView is a type that runs validations on a projected type.
+type RTView struct {
+	A *string
+}
+
+var (
+	// RTMap is a map of attribute names in result type RT indexed by view name.
+	RTMap = map[string][]string{
+		"default": []string{
+			"a",
+		},
+	}
+)
+
+// ValidateRT runs the validations defined on the viewed result type RT.
+func ValidateRT(result *RT) (err error) {
+	switch result.View {
+	case "default", "":
+		err = ValidateRTView(result.Projected)
+	default:
+		err = goa.InvalidEnumValueError("view", result.View, []interface{}{"default"})
+	}
+	return
+}
+
+// ValidateRTView runs the validations defined on RTView using the "default"
+// view.
+func ValidateRTView(result *RTView) (err error) {
+
+	return
+}
+`
diff --git a/codegen/service/testdata/views_dsls.go b/codegen/service/testdata/views_dsls.go
index 475eb500..32cc5e25 100644
--- a/codegen/service/testdata/views_dsls.go
+++ b/codegen/service/testdata/views_dsls.go
@@ -201,6 +201,23 @@ var ResultWithRecursiveCollectionOfResultTypeDSL = func() {
 	})
 }
 
+var ResultWithMultipleMethodsDSL = func() {
+	var RT = ResultType("application/vnd.some_result", func() {
+		TypeName("RT")
+		Attributes(func() {
+			Attribute("a")
+		})
+	})
+	Service("ResultWithMultipleMethods", func() {
+		Method("A", func() {
+			Result(RT)
+		})
+		Method("B", func() {
+			Result(RT)
+		})
+	})
+}
+
 var ResultWithCustomFieldsDSL = func() {
 	var RT = ResultType("application/vnd.result", func() {
 		TypeName("RT")
diff --git a/codegen/service/views_test.go b/codegen/service/views_test.go
index bd3fa876..a07b101a 100644
--- a/codegen/service/views_test.go
+++ b/codegen/service/views_test.go
@@ -24,6 +24,7 @@ func TestViews(t *testing.T) {
 		{"result-with-recursive-result-type", testdata.ResultWithRecursiveResultTypeDSL, testdata.ResultWithRecursiveResultTypeCode},
 		{"result-type-with-custom-fields", testdata.ResultWithCustomFieldsDSL, testdata.ResultWithCustomFieldsCode},
 		{"result-with-recursive-collection-of-result-type", testdata.ResultWithRecursiveCollectionOfResultTypeDSL, testdata.ResultWithRecursiveCollectionOfResultTypeCode},
+		{"result-with-multiple-methods", testdata.ResultWithMultipleMethodsDSL, testdata.ResultWithMultipleMethodsCode},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {