Repository: goadesign/goa
Pull request #2829 (2021-05-03 14:02:08)

Support Meta "struct:field:type" in gRPC
This addresses what appears to be a real bug: if you annotate a field (either streaming or non) used for gRPC, the protobuf type transformation code in convertType failed to respect Meta `struct:field:type`, resulting in the lack of type casts between the requested type and the protobuf mapping thereof, and code that doesn't compile.

It turns out that Meta `struct:field:type` is probably closer to what I wanted for #2498. The implications deserve more thought than I've given them. For example, does a shared design work cleanly across both a client and a server? Do the requested types need to be available to both, or is there a way for one side to use the default type? (Perhaps this was already considered before `struct:field:type` was introduced.)

At a raw implementation level, I'm also unclear how this approach handles the imports on the generated files. It seemed to in my superficial testing, so I'm hoping it's magic I'm not familiar with.

raphael: This looks great, thank you for the PR. Left a comment with a nitpick. I am not sure I follow the questions above wrt sharing such type. As far as generating the import this is done by invoking [AddServiceDataMetaTypeImports](https://github.com/goadesign/goa/blob/20b02e80edc370f29814f129966e0015800146fa/codegen/service/service.go#L154). This function should already be called in all the places where the import is needed.

Patch:
diff --git a/grpc/codegen/protobuf_transform.go b/grpc/codegen/protobuf_transform.go
index a9a3c9c3..2ddaeff3 100644
--- a/grpc/codegen/protobuf_transform.go
+++ b/grpc/codegen/protobuf_transform.go
@@ -465,13 +465,24 @@ func convertType(source, target *expr.AttributeExpr, sourceVar string, ta *trans
 		return fmt.Sprintf("%s(%s)", transformHelperName(source, target, ta), sourceVar)
 	}
 
+	sourceType, _ := codegen.GetMetaType(source)
+	targetType, _ := codegen.GetMetaType(target)
 	if source.Type.Kind() != expr.IntKind && source.Type.Kind() != expr.UIntKind {
+		if sourceType != "" || targetType != "" {
+			if ta.proto || targetType == "" {
+				targetType = protoBufNativeGoTypeName(target.Type)
+			}
+			return fmt.Sprintf("%s(%s)", targetType, sourceVar)
+		}
 		return sourceVar
 	}
 	if ta.proto {
-		return fmt.Sprintf("%s(%s)", protoBufNativeGoTypeName(source.Type), sourceVar)
+		return fmt.Sprintf("%s(%s)", protoBufNativeGoTypeName(target.Type), sourceVar)
+	}
+	if targetType == "" {
+		targetType = codegen.GoNativeTypeName(target.Type)
 	}
-	return fmt.Sprintf("%s(%s)", codegen.GoNativeTypeName(source.Type), sourceVar)
+	return fmt.Sprintf("%s(%s)", targetType, sourceVar)
 }
 
 // zeroValure returns the zero value for the given primitive type.
@@ -569,9 +580,7 @@ func transformAttributeHelpers(source, target *expr.AttributeExpr, ta *transform
 // collectHelpers recursively traverses the given attributes and return the
 // transform helper functions required to generate the transform code.
 func collectHelpers(source, target *expr.AttributeExpr, req bool, ta *transformAttrs, seen ...map[string]*codegen.TransformFunctionData) ([]*codegen.TransformFunctionData, error) {
-	var (
-		data []*codegen.TransformFunctionData
-	)
+	var data []*codegen.TransformFunctionData
 	switch {
 	case expr.IsArray(source.Type):
 		helpers, err := transformAttributeHelpers(


Test patch:
diff --git a/codegen/testdata/types_dsl.go b/codegen/testdata/types_dsl.go
index 62643eb0..4f94d526 100644
--- a/codegen/testdata/types_dsl.go
+++ b/codegen/testdata/types_dsl.go
@@ -13,6 +13,20 @@ var TestTypesDSL = func() {
 			Required("required_string")
 		})
 
+		_ = Type("CustomTypes", func() {
+			Attribute("required_string", String, func() {
+				Meta("struct:field:type", "tdtypes.CustomString", "goa.design/goa/v3/codegen/testdata/tdtypes")
+			})
+			Attribute("default_bool", Boolean, func() {
+				Meta("struct:field:type", "tdtypes.CustomBool", "goa.design/goa/v3/codegen/testdata/tdtypes")
+				Default(true)
+			})
+			Attribute("integer", Int, func() {
+				Meta("struct:field:type", "tdtypes.CustomInt", "goa.design/goa/v3/codegen/testdata/tdtypes")
+			})
+			Required("required_string")
+		})
+
 		_ = Type("Required", func() {
 			Extend(Simple)
 			Required("required_string", "default_bool", "integer")
diff --git a/grpc/codegen/protobuf_transform_test.go b/grpc/codegen/protobuf_transform_test.go
index 1240de35..f9978bc4 100644
--- a/grpc/codegen/protobuf_transform_test.go
+++ b/grpc/codegen/protobuf_transform_test.go
@@ -16,9 +16,10 @@ func TestProtoBufTransform(t *testing.T) {
 		// types to test
 		primitive = expr.Int
 
-		simple   = root.UserType("Simple")
-		required = root.UserType("Required")
-		defaultT = root.UserType("Default")
+		simple     = root.UserType("Simple")
+		required   = root.UserType("Required")
+		defaultT   = root.UserType("Default")
+		customtype = root.UserType("CustomTypes")
 
 		simpleMap  = root.UserType("SimpleMap")
 		nestedMap  = root.UserType("NestedMap")
@@ -63,6 +64,8 @@ func TestProtoBufTransform(t *testing.T) {
 			{"simple-to-default", simple, defaultT, true, svcCtx, simpleSvcToDefaultProtoCode},
 			{"default-to-simple", defaultT, simple, true, svcCtx, defaultSvcToSimpleProtoCode},
 			{"required-ptr-to-simple", required, simple, true, ptrCtx, requiredPtrSvcToSimpleProtoCode},
+			{"simple-to-customtype", customtype, simple, true, svcCtx, customSvcToSimpleProtoCode},
+			{"customtype-to-customtype", customtype, customtype, true, svcCtx, customSvcToCustomProtoCode},
 
 			// maps
 			{"map-to-map", simpleMap, simpleMap, true, svcCtx, simpleMapSvcToSimpleMapProtoCode},
@@ -95,6 +98,8 @@ func TestProtoBufTransform(t *testing.T) {
 			{"simple-to-default", simple, defaultT, false, svcCtx, simpleProtoToDefaultSvcCode},
 			{"default-to-simple", defaultT, simple, false, svcCtx, defaultProtoToSimpleSvcCode},
 			{"simple-to-required-ptr", simple, required, false, ptrCtx, simpleProtoToRequiredPtrSvcCode},
+			{"simple-to-customtype", simple, customtype, false, svcCtx, simpleProtoToCustomSvcCode},
+			{"customtype-to-customtype", customtype, customtype, false, svcCtx, customProtoToCustomSvcCode},
 
 			// maps
 			{"map-to-map", simpleMap, simpleMap, false, svcCtx, simpleMapProtoToSimpleMapSvcCode},
@@ -228,6 +233,42 @@ const (
 }
 `
 
+	customSvcToSimpleProtoCode = `func transform() {
+	target := &Simple{
+		RequiredString: string(source.RequiredString),
+		DefaultBool:    bool(source.DefaultBool),
+	}
+	if source.Integer != nil {
+		target.Integer = int32(*source.Integer)
+	}
+}
+`
+
+	simpleProtoToCustomSvcCode = `func transform() {
+	target := &CustomTypes{
+		RequiredString: tdtypes.CustomString(source.RequiredString),
+		DefaultBool:    tdtypes.CustomBool(source.DefaultBool),
+	}
+	if source.Integer != 0 {
+		integerptr := tdtypes.CustomInt(source.Integer)
+		target.Integer = &integerptr
+	}
+}
+`
+
+	customSvcToCustomProtoCode = `func transform() {
+	target := &CustomTypes{
+		RequiredString: string(source.RequiredString),
+		DefaultBool:    bool(source.DefaultBool),
+	}
+	if source.Integer != nil {
+		target.Integer = int32(*source.Integer)
+	}
+}
+`
+
+	customProtoToCustomSvcCode = simpleProtoToCustomSvcCode
+
 	simpleMapSvcToSimpleMapProtoCode = `func transform() {
 	target := &SimpleMap{}
 	if source.Simple != nil {