Repository: goadesign/goa
Pull request #2701 (2020-10-31 01:05:18)

Fix potential panic with embedded result types
Finalize was not called on a result type embedded within another result
type and that is not used directly by a method.

Finalize now recurses through all child attributes.


Patch:
diff --git a/expr/attribute.go b/expr/attribute.go
index 6ca46ef4..538d02cc 100644
--- a/expr/attribute.go
+++ b/expr/attribute.go
@@ -541,6 +541,17 @@ func (a *AttributeExpr) debug(prefix string, seen map[*AttributeExpr]int, indent
 		m.KeyType.debug("key", seen, indent+1)
 		m.ElemType.debug("elem", seen, indent+1)
 	}
+	if rt, ok := a.Type.(*ResultTypeExpr); ok {
+		fmt.Printf("%sviews\n", tab)
+		for _, v := range rt.Views {
+			nats := *AsObject(v.AttributeExpr.Type)
+			keys := make([]string, len(nats))
+			for i, n := range nats {
+				keys[i] = n.Name
+			}
+			fmt.Printf("%s- %s: %v\n", tab+"  ", v.Name, keys)
+		}
+	}
 	if d := a.DefaultValue; d != nil {
 		fmt.Printf("%sdefault\n", tab)
 		fmt.Printf("%s%#v", tab+"  ", a.DefaultValue)
diff --git a/expr/http_body_types.go b/expr/http_body_types.go
index f484707b..5e7d3c29 100644
--- a/expr/http_body_types.go
+++ b/expr/http_body_types.go
@@ -268,18 +268,19 @@ func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseE
 	}
 }
 
-// buildBodyTypeName concatenates the given strings to generate the
-// endpoint's body type name.
-//
+// concat concatenates the given strings with "smart(?) casing".
 // The concatenation algorithm is:
+//
 // 1) If the first string contains underscores and starts with a lower case,
 // the rest of the strings are converted to lower case and concatenated with
 // underscores.
 // e.g. concat("my_endpoint", "Request", "BODY") => "my_endpoint_request_body"
+//
 // 2) If the first string contains underscores and starts with a upper case,
 // the rest of the strings are converted to title case and concatenated with
 // underscores.
 // e.g. concat("My_endpoint", "response", "body") => "My_endpoint_Response_Body"
+//
 // 3) If the first string is a single word or camelcased, the rest of the
 // strings are concatenated to form a valid upper camelcase.
 // e.g. concat("myEndpoint", "streaming", "Body") => "MyEndpointStreamingBody"
diff --git a/expr/result_type.go b/expr/result_type.go
index 1dcda6eb..647e1caf 100644
--- a/expr/result_type.go
+++ b/expr/result_type.go
@@ -181,6 +181,24 @@ func (m *ResultTypeExpr) ViewHasAttribute(view, attr string) bool {
 // Finalize builds the default view if not explicitly defined and finalizes
 // the underlying UserTypeExpr.
 func (m *ResultTypeExpr) Finalize() {
+	if m.View("default") == nil {
+		m.ensureDefaultView()
+	}
+	m.UserTypeExpr.Finalize()
+	seen := make(map[string]struct{})
+	walkAttribute(m.AttributeExpr, func(_ string, att *AttributeExpr) error {
+		if rt, ok := att.Type.(*ResultTypeExpr); ok {
+			if _, ok := seen[rt.Identifier]; !ok {
+				seen[rt.Identifier] = struct{}{}
+				rt.ensureDefaultView()
+			}
+		}
+		return nil
+	})
+}
+
+// ensureDefaultView builds the default view if not explicitly defined.
+func (m *ResultTypeExpr) ensureDefaultView() {
 	if m.View("default") == nil {
 		att := DupAtt(m.AttributeExpr)
 		if arr := AsArray(att.Type); arr != nil {
@@ -193,7 +211,6 @@ func (m *ResultTypeExpr) Finalize() {
 		}
 		m.Views = append(m.Views, v)
 	}
-	m.UserTypeExpr.Finalize()
 }
 
 // Project creates a ResultTypeExpr containing the fields defined in the view


Test patch:
diff --git a/http/codegen/server_types_test.go b/http/codegen/server_types_test.go
index 49fad0f7..c78fe7e9 100644
--- a/http/codegen/server_types_test.go
+++ b/http/codegen/server_types_test.go
@@ -21,6 +21,7 @@ func TestServerTypes(t *testing.T) {
 		{"payload-extend-validate", testdata.PayloadExtendedValidateDSL, PayloadExtendedValidateServerTypesFile},
 		{"result-type-validate", testdata.ResultTypeValidateDSL, ResultTypeValidateServerTypesFile},
 		{"with-result-collection", testdata.ResultWithResultCollectionDSL, ResultWithResultCollectionServerTypesFile},
+		{"with-result-view", testdata.ResultWithResultViewDSL, ResultWithResultViewServerTypesFile},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
@@ -273,3 +274,30 @@ func NewMethodResultWithResultCollectionResponseBody(res *serviceresultwithresul
 	return body
 }
 `
+
+const ResultWithResultViewServerTypesFile = `// MethodResultWithResultViewResponseBodyFull is the type of the
+// "ServiceResultWithResultView" service "MethodResultWithResultView" endpoint
+// HTTP response body.
+type MethodResultWithResultViewResponseBodyFull struct {
+	Name *string         ` + "`" + `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + "`" + `
+	Rt   *RtResponseBody ` + "`" + `form:"rt,omitempty" json:"rt,omitempty" xml:"rt,omitempty"` + "`" + `
+}
+
+// RtResponseBody is used to define fields on response body types.
+type RtResponseBody struct {
+	X *string ` + "`" + `form:"x,omitempty" json:"x,omitempty" xml:"x,omitempty"` + "`" + `
+}
+
+// NewMethodResultWithResultViewResponseBodyFull builds the HTTP response body
+// from the result of the "MethodResultWithResultView" endpoint of the
+// "ServiceResultWithResultView" service.
+func NewMethodResultWithResultViewResponseBodyFull(res *serviceresultwithresultviewviews.ResulttypeView) *MethodResultWithResultViewResponseBodyFull {
+	body := &MethodResultWithResultViewResponseBodyFull{
+		Name: res.Name,
+	}
+	if res.Rt != nil {
+		body.Rt = marshalServiceresultwithresultviewviewsRtViewToRtResponseBody(res.Rt)
+	}
+	return body
+}
+`
diff --git a/http/codegen/testdata/result_dsls.go b/http/codegen/testdata/result_dsls.go
index e1acbd59..8b91c920 100644
--- a/http/codegen/testdata/result_dsls.go
+++ b/http/codegen/testdata/result_dsls.go
@@ -744,6 +744,38 @@ var ResultWithResultCollectionDSL = func() {
 	})
 }
 
+var ResultWithResultViewDSL = func() {
+	var RT = ResultType("RT", func() {
+		Attributes(func() {
+			Attribute("x")
+		})
+	})
+	var ResultType = ResultType("ResultType", func() {
+		Attributes(func() {
+			Attribute("name")
+			Attribute("rt", RT)
+		})
+		View("full", func() {
+			Attribute("name")
+			Attribute("rt")
+		})
+		View("default", func() {
+			Attribute("name")
+		})
+	})
+	Service("ServiceResultWithResultView", func() {
+		Method("MethodResultWithResultView", func() {
+			Result(ResultType, func() {
+				View("full")
+			})
+			HTTP(func() {
+				POST("/")
+				Response(StatusOK)
+			})
+		})
+	})
+}
+
 var EmptyBodyResultMultipleViewsDSL = func() {
 	var ResultType = ResultType("ResultTypeMultipleViews", func() {
 		Attribute("a", String)