Repository: goadesign/goa (go)

Issue:
Goa generating uncompilable code while using views on types that contain OneOf union type
Complete design
``` 
package design

import (
	. "goa.design/goa/v3/dsl"
)

var _ = Service("Cars Service", func() {
	Description(`Test Cars Service`)

	Method("get_car", func() {
		Description("Fetches list of cars.")
		Payload(func() {
			Field(1, "ID", Int64, "ID of the car")
			Field(2, "View", String, "specifies the view of the car", func() {
				Enum("default", "extended")
				Default("default")
			})
			Required("ID")
		})
		Result(Car)

		Error("internal")
		GRPC(func() {
			Response(CodeOK)
			Response("internal", CodeInternal)
		})
	})
})

var Car = ResultType("Car", "Car", func() {
	Description("Test Car")

	Field(1, "ID", Int64, "ID of the car")
	Field(2, "Name", String, "Name of the car")
	Field(3, "Type", CarType, "Model of the car")
	Field(4, "Year", Int, "Year of the car")

	View("default", func() {
		Attribute("ID")
		Attribute("Name")
	})

	View("extended", func() {
		Attribute("ID")
		Attribute("Name")
		Attribute("Type")
		Attribute("Year")
	})

	Required("ID", "Name")
})

var CarType = Type("CarType", func() {
	Description("Car Type")

	OneOf("Type", func() {
		Field(1, "Sedan", Sedan, "Type of the car")
		Field(2, "SUV", SUV, "Type of the car")
	})

	Required("Type")
})

var Sedan = Type("Sedan", func() {
	Description("Sedan Type")

	Field(1, "DriveType", String, "Model of the car")
	Field(2, "Seats", SeatCount, "Number of seats")
})

var SUV = Type("SUV", func() {
	Description("SUV Type")

	Field(1, "DriveType", String, "Model of the car")
	Field(2, "Seats", SeatCount, "Number of seats")
})

var SeatCount = Type("SeatCount", func() {
	Description("Seat Count")

	Field(1, "Seats", Int, "Number of seats")

	Required("Seats")
})
```

Invalid code generated
```
// ValidateCarTypeView runs the validations defined on CarTypeView.
func ValidateCarTypeView(result *CarTypeView) (err error) {
	if result.Type == nil {
		err = goa.MergeErrors(err, goa.MissingFieldError("Type", "result"))
	}
	switch v := result.Type.(type) {
	case *CarTypeView_Sedan:
		if v.Sedan != nil {
			if err2 := ValidateSedanView(v.Sedan); err2 != nil {
				err = goa.MergeErrors(err, err2)
			}
		}

	case *CarTypeView_SUV:
		if v.SUV != nil {
			if err2 := ValidateSUVView(v.SUV); err2 != nil {
				err = goa.MergeErrors(err, err2)
			}
		}
	}
	return
}
```
The types `*CarTypeView_Sedan` and `CarTypeView_SUV` are not generated.

Note: The generated code works fine as long as the Union types `Sedan` and `SUV` do not contain any user defined types i.e if we make `Seats` a `String` as opposed to `SeatCount`

Pull request description:
Fix validation code for projected union types
Fix #3486

Patch:
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 701ba657..5f614322 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -1688,7 +1688,7 @@ func buildValidations(projected *expr.AttributeExpr, scope *codegen.NameScope) [
 					})
 					ctx = projectedTypeContext("", !expr.IsPrimitive(projected.Type), scope)
 				}
-				data["Validate"] = codegen.ValidationCode(&expr.AttributeExpr{Type: o, Validation: rt.Validation}, rt, ctx, true, false, "result")
+				data["Validate"] = codegen.ValidationCode(&expr.AttributeExpr{Type: o, Validation: rt.Validation}, rt, ctx, true, false, true, "result")
 				data["Fields"] = fields
 			}
 
@@ -1713,7 +1713,7 @@ func buildValidations(projected *expr.AttributeExpr, scope *codegen.NameScope) [
 			Name:        name,
 			Description: fmt.Sprintf("%s runs the validations defined on %s.", name, tname),
 			Ref:         scope.GoTypeRef(projected),
-			Validate:    codegen.ValidationCode(ut.Attribute(), ut, ctx, true, expr.IsAlias(ut), "result"),
+			Validate:    codegen.ValidationCode(ut.Attribute(), ut, ctx, true, expr.IsAlias(ut), true, "result"),
 		})
 	}
 	return validations
diff --git a/codegen/validation.go b/codegen/validation.go
index 2ff933cd..881019c4 100644
--- a/codegen/validation.go
+++ b/codegen/validation.go
@@ -50,7 +50,7 @@ func init() {
 // See ValidationCode for a description of the arguments.
 func AttributeValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias bool, target, attName string) string {
 	seen := make(map[string]*bytes.Buffer)
-	return recurseValidationCode(att, put, attCtx, req, alias, target, attName, seen).String()
+	return recurseValidationCode(att, put, attCtx, req, alias, false, target, attName, seen).String()
 }
 
 // ValidationCode produces Go code that runs the validations defined in the
@@ -66,15 +66,19 @@ func AttributeValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx
 //
 // alias indicates whether the attribute is an alias user type attribute.
 //
+// view indicates whether the attribute is a view type attribute.
+// This only matters for union types: generated Goa view union types have a
+// different layout than proto generated union types.
+//
 // target is the variable name against which the validation code is generated
 //
 // context is used to produce helpful messages in case of error.
-func ValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias bool, target string) string {
+func ValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias, view bool, target string) string {
 	seen := make(map[string]*bytes.Buffer)
-	return recurseValidationCode(att, put, attCtx, req, alias, target, target, seen).String()
+	return recurseValidationCode(att, put, attCtx, req, alias, view, target, target, seen).String()
 }
 
-func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias bool, target, context string, seen map[string]*bytes.Buffer) *bytes.Buffer {
+func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *AttributeContext, req, alias, view bool, target, context string, seen map[string]*bytes.Buffer) *bytes.Buffer {
 	var (
 		buf      = new(bytes.Buffer)
 		first    = true
@@ -115,7 +119,7 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 		for _, nat := range *(expr.AsObject(att.Type)) {
 			tgt := fmt.Sprintf("%s.%s", target, attCtx.Scope.Field(nat.Attribute, nat.Name, true))
 			ctx := fmt.Sprintf("%s.%s", context, nat.Name)
-			val := validateAttribute(attCtx, nat.Attribute, put, tgt, ctx, att.IsRequired(nat.Name))
+			val := validateAttribute(attCtx, nat.Attribute, put, tgt, ctx, att.IsRequired(nat.Name), view)
 			if val != "" {
 				newline()
 				buf.WriteString(val)
@@ -129,7 +133,7 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 			ctx = attCtx.Dup()
 			ctx.Pointer = false
 		}
-		val := validateAttribute(ctx, elem, put, "e", context+"[*]", true)
+		val := validateAttribute(ctx, elem, put, "e", context+"[*]", true, view)
 		if val != "" {
 			newline()
 			data := map[string]any{"target": target, "validation": val}
@@ -141,11 +145,11 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 		m := expr.AsMap(att.Type)
 		ctx := attCtx.Dup()
 		ctx.Pointer = false
-		keyVal := validateAttribute(ctx, m.KeyType, put, "k", context+".key", true)
+		keyVal := validateAttribute(ctx, m.KeyType, put, "k", context+".key", true, view)
 		if keyVal != "" {
 			keyVal = "\n" + keyVal
 		}
-		valueVal := validateAttribute(ctx, m.ElemType, put, "v", context+"[key]", true)
+		valueVal := validateAttribute(ctx, m.ElemType, put, "v", context+"[key]", true, view)
 		if valueVal != "" {
 			valueVal = "\n" + valueVal
 		}
@@ -158,19 +162,27 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 		}
 	case expr.IsUnion(att.Type):
 		// NOTE: the only time we validate a union is when we are
-		// validating a proto-generated type since the HTTP
+		// validating a proto-generated type or view types since the HTTP
 		// serialization transforms unions into objects.
 		u := expr.AsUnion(att.Type)
-		tref := attCtx.Scope.Ref(&expr.AttributeExpr{Type: put}, attCtx.DefaultPkg)
 		var vals []string
 		var types []string
 		for _, v := range u.Values {
 			vatt := v.Attribute
-			fieldName := attCtx.Scope.Field(vatt, v.Name, true)
-			val := validateAttribute(attCtx, vatt, put, "v."+fieldName, context+".value", true)
-			if val != "" {
-				types = append(types, tref+"_"+fieldName)
-				vals = append(vals, val)
+			if view {
+				val := validateAttribute(attCtx, vatt, put, "v", context+".value", true, view)
+				if val != "" {
+					types = append(types, attCtx.Scope.Ref(vatt, attCtx.DefaultPkg))
+					vals = append(vals, val)
+				}
+			} else {
+				fieldName := attCtx.Scope.Field(vatt, v.Name, true)
+				val := validateAttribute(attCtx, vatt, put, "v."+fieldName, context+".value", true, view)
+				if val != "" {
+					tref := attCtx.Scope.Ref(&expr.AttributeExpr{Type: put}, attCtx.DefaultPkg)
+					types = append(types, tref+"_"+fieldName)
+					vals = append(vals, val)
+				}
 			}
 		}
 		if len(vals) > 0 {
@@ -189,10 +201,10 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 	return buf
 }
 
-func validateAttribute(ctx *AttributeContext, att *expr.AttributeExpr, put expr.UserType, target, context string, req bool) string {
+func validateAttribute(ctx *AttributeContext, att *expr.AttributeExpr, put expr.UserType, target, context string, req, view bool) string {
 	ut, isUT := att.Type.(expr.UserType)
 	if !isUT {
-		code := recurseValidationCode(att, put, ctx, req, false, target, context, nil).String()
+		code := recurseValidationCode(att, put, ctx, req, false, view, target, context, nil).String()
 		if code == "" {
 			return ""
 		}
@@ -209,7 +221,7 @@ func validateAttribute(ctx *AttributeContext, att *expr.AttributeExpr, put expr.
 		return fmt.Sprintf("%s%s\n}", cond, code)
 	}
 	if expr.IsAlias(ut) {
-		return recurseValidationCode(ut.Attribute(), put, ctx, req, true, target, context, nil).String()
+		return recurseValidationCode(ut.Attribute(), put, ctx, req, true, view, target, context, nil).String()
 	}
 	if !hasValidations(ctx, ut) {
 		return ""
@@ -235,6 +247,10 @@ func validateAttribute(ctx *AttributeContext, att *expr.AttributeExpr, put expr.
 //
 // alias indicates whether the attribute is an alias user type attribute.
 //
+// view indicates whether the attribute is a view type attribute.
+// This only matters for union types: generated Goa view union types have a
+// different layout than proto generated union types.
+//
 // target is the variable name against which the validation code is generated
 //
 // context is used to produce helpful messages in case of error.
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index f356d441..7514737c 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -745,7 +745,7 @@ func addValidation(att *expr.AttributeExpr, attName string, sd *ServiceData, req
 	}
 	vtx := protoBufTypeContext(sd.PkgName, sd.Scope, false)
 	removeMeta(att)
-	if def := codegen.ValidationCode(att, ut, vtx, true, expr.IsAlias(att.Type), attName); def != "" {
+	if def := codegen.ValidationCode(att, ut, vtx, true, expr.IsAlias(att.Type), false, attName); def != "" {
 		v := &ValidationData{
 			Name:    "Validate" + name,
 			Def:     def,
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 118333b4..cee5a002 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -1139,8 +1139,8 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 			)
 			if ut, ok := body.(expr.UserType); ok {
 				if val := ut.Attribute().Validation; val != nil {
-					svcode = codegen.ValidationCode(ut.Attribute(), ut, httpsvrctx, true, expr.IsAlias(ut), "body")
-					cvcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), "body")
+					svcode = codegen.ValidationCode(ut.Attribute(), ut, httpsvrctx, true, expr.IsAlias(ut), false, "body")
+					cvcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), false, "body")
 				}
 			}
 			serverArgs = []*InitArgData{{
@@ -1280,7 +1280,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							TypeRef:      uref,
 							Type:         uatt.Type,
 							Pointer:      sc.UsernamePointer,
-							Validate:     codegen.ValidationCode(uatt, nil, httpsvrctx, sc.UsernameRequired, expr.IsAlias(uatt.Type), sc.UsernameAttr),
+							Validate:     codegen.ValidationCode(uatt, nil, httpsvrctx, sc.UsernameRequired, expr.IsAlias(uatt.Type), false, sc.UsernameAttr),
 							Example:      uatt.Example(expr.Root.API.ExampleGenerator),
 						},
 					}
@@ -1303,7 +1303,7 @@ func buildPayloadData(e *expr.HTTPEndpointExpr, sd *ServiceData) *PayloadData {
 							TypeRef:      pref,
 							Type:         patt.Type,
 							Pointer:      sc.PasswordPointer,
-							Validate:     codegen.ValidationCode(patt, nil, httpsvrctx, sc.PasswordRequired, expr.IsAlias(patt.Type), sc.PasswordAttr),
+							Validate:     codegen.ValidationCode(patt, nil, httpsvrctx, sc.PasswordRequired, expr.IsAlias(patt.Type), false, sc.PasswordAttr),
 							Example:      patt.Example(expr.Root.API.ExampleGenerator),
 						},
 					}
@@ -1617,7 +1617,7 @@ func buildResponses(e *expr.HTTPEndpointExpr, result *expr.AttributeExpr, viewed
 							var vcode string
 							if ut, ok := resp.Body.Type.(expr.UserType); ok {
 								if val := ut.Attribute().Validation; val != nil {
-									vcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), "body")
+									vcode = codegen.ValidationCode(ut.Attribute(), ut, httpclictx, true, expr.IsAlias(ut), false, "body")
 								}
 							}
 							clientArgs = []*InitArgData{{
@@ -2008,7 +2008,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 				varname, svc.Name, e.Name())
 			if svr {
 				// generate validation code for unmarshaled type (server-side).
-				validateDef = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), "body")
+				validateDef = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), false, "body")
 				if validateDef != "" {
 					validateRef = fmt.Sprintf("err = Validate%s(&body)", varname)
 				}
@@ -2023,7 +2023,7 @@ func buildRequestBodyType(body, att *expr.AttributeExpr, e *expr.HTTPEndpointExp
 			}
 			varname = sd.Scope.GoTypeRef(body)
 			ctx := codegen.NewAttributeContext(false, false, !svr, "", sd.Scope)
-			validateRef = codegen.ValidationCode(body, nil, ctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, ctx, true, expr.IsAlias(body.Type), false, "body")
 			desc = body.Description
 		}
 	}
@@ -2158,7 +2158,7 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 				varname, svc.Name, e.Name())
 			if !svr && view == nil {
 				// generate validation code for unmarshaled type (client-side).
-				validateDef = codegen.ValidationCode(body, ut, httpctx, true, expr.IsAlias(body.Type), "body")
+				validateDef = codegen.ValidationCode(body, ut, httpctx, true, expr.IsAlias(body.Type), false, "body")
 				if validateDef != "" {
 					target := "&body"
 					if expr.IsArray(ut) {
@@ -2175,12 +2175,12 @@ func buildResponseBodyType(body, att *expr.AttributeExpr, loc *codegen.Location,
 			desc = fmt.Sprintf("%s is the type of the %q service %q endpoint HTTP response body.",
 				varname, svc.Name, e.Name())
 			def = goTypeDef(sd.Scope, body, !svr, svr)
-			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), false, "body")
 		} else {
 			// response body is a primitive type. They are used as non-pointers when
 			// encoding/decoding responses.
 			httpctx = httpContext("", sd.Scope, false, true)
-			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), "body")
+			validateRef = codegen.ValidationCode(body, nil, httpctx, true, expr.IsAlias(body.Type), false, "body")
 			varname = sd.Scope.GoTypeRef(body)
 			desc = body.Description
 		}
@@ -2627,7 +2627,7 @@ func attributeTypeData(ut expr.UserType, req, ptr, server bool, rd *ServiceData)
 		if req || !req && !server {
 			// generate validations for responses client-side and for
 			// requests server-side and CLI
-			validate = codegen.ValidationCode(ut.Attribute(), ut, hctx, true, expr.IsAlias(ut), "body")
+			validate = codegen.ValidationCode(ut.Attribute(), ut, hctx, true, expr.IsAlias(ut), false, "body")
 		}
 		if validate != "" {
 			validateRef = fmt.Sprintf("err = Validate%s(v)", name)
diff --git a/http/codegen/websocket.go b/http/codegen/websocket.go
index ec164df8..59f52f7b 100644
--- a/http/codegen/websocket.go
+++ b/http/codegen/websocket.go
@@ -127,7 +127,7 @@ func initWebSocketData(ed *EndpointData, e *expr.HTTPEndpointExpr, sd *ServiceDa
 							if ut, ok := body.(expr.UserType); ok {
 								if val := ut.Attribute().Validation; val != nil {
 									httpctx := httpContext("", sd.Scope, true, true)
-									svcode = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), "body")
+									svcode = codegen.ValidationCode(ut.Attribute(), ut, httpctx, true, expr.IsAlias(ut), false, "body")
 								}
 							}
 						}


Test patch:
diff --git a/codegen/testdata/validation_code.go b/codegen/testdata/validation_code.go
index a6b91831..9032b10a 100644
--- a/codegen/testdata/validation_code.go
+++ b/codegen/testdata/validation_code.go
@@ -492,6 +492,57 @@ const (
 		}
 	}
 }
+`
+
+	UnionWithViewValidationCode = `func Validate() (err error) {
+	if target.RequiredUnion == nil {
+		err = goa.MergeErrors(err, goa.MissingFieldError("required_union", "target"))
+	}
+	switch v := target.RequiredUnion.(type) {
+	case *Integer:
+		if v != nil {
+			if err2 := ValidateInteger(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Float:
+		if v != nil {
+			if err2 := ValidateFloat(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *String:
+		if v != nil {
+			if err2 := ValidateString(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+	}
+	switch v := target.Union.(type) {
+	case *Integer:
+		if v != nil {
+			if err2 := ValidateInteger(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *Float:
+		if v != nil {
+			if err2 := ValidateFloat(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+
+	case *String:
+		if v != nil {
+			if err2 := ValidateString(v); err2 != nil {
+				err = goa.MergeErrors(err, err2)
+			}
+		}
+	}
+}
 `
 
 	ResultTypePointerValidationCode = `func Validate() (err error) {
diff --git a/codegen/validation_test.go b/codegen/validation_test.go
index 6823675b..84b99140 100644
--- a/codegen/validation_test.go
+++ b/codegen/validation_test.go
@@ -64,11 +64,20 @@ func TestRecursiveValidationCode(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			ctx := NewAttributeContext(c.Pointer, false, c.UseDefault, "", scope)
-			code := ValidationCode(&expr.AttributeExpr{Type: c.Type}, nil, ctx, c.Required, expr.IsAlias(c.Type), "target")
+			code := ValidationCode(&expr.AttributeExpr{Type: c.Type}, nil, ctx, c.Required, expr.IsAlias(c.Type), false, "target")
 			code = FormatTestCode(t, "package foo\nfunc Validate() (err error){\n"+code+"}")
 			if code != c.Code {
 				t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, c.Code))
 			}
 		})
 	}
+	// Special case of unions with views
+	t.Run("union-with-view", func(t *testing.T) {
+		ctx := NewAttributeContext(false, false, false, "", scope)
+		code := ValidationCode(&expr.AttributeExpr{Type: unionT}, nil, ctx, true, false, true, "target")
+		code = FormatTestCode(t, "package foo\nfunc Validate() (err error){\n"+code+"}")
+		if code != testdata.UnionWithViewValidationCode {
+			t.Errorf("invalid code, got:\n%s\ngot vs. expected:\n%s", code, Diff(t, code, testdata.UnionWithViewValidationCode))
+		}
+	})
 }