Repository: goadesign/goa
Pull request #3251 (2023-02-27 12:25:13)

Fix syntax of dsl.Meta("struct:field:external") from dots to colons
I think it's just a typo but this fix is a breaking change.
It may be better to leave both new and old syntax for a while.

Note: Only one place was already colons.

https://github.com/goadesign/goa/blob/ed6dd56f56945c2d526b77835b6e20ab897f5ce1/grpc/codegen/protobuf_transform.go#L113-L119

raphael: Good catch, thank you. Yes, I suggest we keep supporting the old syntax but only document the new one.
tchssk: I restored the deprecated syntax. 31d7fed3e2b94beaac43b2bb85fca291b6f2756b
raphael: Thank you!

Patch:
diff --git a/codegen/service/convert.go b/codegen/service/convert.go
index a70b2951..93b5b36f 100644
--- a/codegen/service/convert.go
+++ b/codegen/service/convert.go
@@ -449,7 +449,12 @@ func buildDesignType(dt *expr.DataType, t reflect.Type, ref expr.DataType, recs
 			atn, _ := attributeName(oref, f.Name)
 			if oref != nil {
 				if at := oref.Attribute(atn); at != nil {
-					if m := at.Meta["struct.field.external"]; len(m) > 0 {
+					if m := at.Meta["struct:field:external"]; len(m) > 0 {
+						if m[0] == "-" {
+							continue
+						}
+					}
+					if m := at.Meta["struct.field.external"]; len(m) > 0 { // Deprecated syntax. Only present for backward compatibility.
 						if m[0] == "-" {
 							continue
 						}
@@ -533,8 +538,15 @@ func attributeName(obj *expr.Object, name string) (string, string) {
 	if obj == nil {
 		return name, ""
 	}
-	// first look for a "struct.field.external" meta
+	// first look for a "struct:field:external" meta
 	for _, nat := range *obj {
+		if m := nat.Attribute.Meta["struct:field:external"]; len(m) > 0 {
+			if m[0] == name {
+				return nat.Name, name
+			}
+		}
+	}
+	for _, nat := range *obj { // Deprecated syntax. Only present for backward compatibility.
 		if m := nat.Attribute.Meta["struct.field.external"]; len(m) > 0 {
 			if m[0] == name {
 				return nat.Name, name
@@ -685,7 +697,13 @@ func compatible(from expr.DataType, to reflect.Type, recs ...compRec) error {
 				field reflect.StructField
 			)
 			{
-				if ef, k := nat.Attribute.Meta["struct.field.external"]; k {
+				if ef, k := nat.Attribute.Meta["struct:field:external"]; k {
+					fname = ef[0]
+					if fname == "-" {
+						continue
+					}
+					field, ok = to.FieldByName(ef[0])
+				} else if ef, k := nat.Attribute.Meta["struct.field.external"]; k { // Deprecated syntax. Only present for backward compatibility.
 					fname = ef[0]
 					if fname == "-" {
 						continue
diff --git a/dsl/convert.go b/dsl/convert.go
index 16c2f762..cb1ddbf1 100644
--- a/dsl/convert.go
+++ b/dsl/convert.go
@@ -10,14 +10,14 @@ import (
 // it possible to instantiate the external type. The default algorithm used to
 // match the external type fields to the design attributes is as follows:
 //
-//    1. Look for an attribute with the same name as the field
-//    2. Look for an attribute with the same name as the field but with the
-//       first letter being lowercase
-//    3. Look for an attribute with a name corresponding to the snake_case
-//       version of the field name
+//  1. Look for an attribute with the same name as the field
+//  2. Look for an attribute with the same name as the field but with the
+//     first letter being lowercase
+//  3. Look for an attribute with a name corresponding to the snake_case
+//     version of the field name
 //
 // This algorithm does not apply if the attribute is equipped with the
-// "struct.field.external" meta. In this case the matching is done by
+// "struct:field:external" meta. In this case the matching is done by
 // looking up the field with a name corresponding to the value of the meta.
 // If the value of the meta is "-" the attribute isn't matched and no
 // conversion code is generated for it. In all other cases it is an error if no
@@ -27,8 +27,8 @@ import (
 // The following limitations apply on the external Go struct field types
 // recursively:
 //
-//    * struct fields must use pointers
-//    * pointers on slices or on maps are not supported
+//   - struct fields must use pointers
+//   - pointers on slices or on maps are not supported
 //
 // ConvertTo must appear in Type or ResutType.
 //
@@ -38,35 +38,34 @@ import (
 //
 // Service design:
 //
-//    var Bottle = Type("bottle", func() {
-//        Description("A bottle")
-//        ConvertTo(models.Bottle{})
-//        // The "rating" attribute is matched to the external
-//        // typ "Rating" field.
-//        Attribute("rating", Int)
-//        Attribute("name", String, func() {
-//            // The "name" attribute is matched to the external
-//            // type "MyName" field.
-//            Meta("struct.field.external", "MyName")
-//        })
-//        Attribute("vineyard", String, func() {
-//            // The "vineyard" attribute is not converted.
-//            Meta("struct.field.external", "-")
-//        })
-//    })
+//	var Bottle = Type("bottle", func() {
+//	    Description("A bottle")
+//	    ConvertTo(models.Bottle{})
+//	    // The "rating" attribute is matched to the external
+//	    // typ "Rating" field.
+//	    Attribute("rating", Int)
+//	    Attribute("name", String, func() {
+//	        // The "name" attribute is matched to the external
+//	        // type "MyName" field.
+//	        Meta("struct:field:external", "MyName")
+//	    })
+//	    Attribute("vineyard", String, func() {
+//	        // The "vineyard" attribute is not converted.
+//	        Meta("struct:field:external", "-")
+//	    })
+//	})
 //
 // External (i.e. non design) package:
 //
-//    package model
-//
-//    type Bottle struct {
-//        Rating int
-//        // Mapped field
-//        MyName string
-//        // Additional fields are OK
-//        Description string
-//    }
+//	package model
 //
+//	type Bottle struct {
+//	    Rating int
+//	    // Mapped field
+//	    MyName string
+//	    // Additional fields are OK
+//	    Description string
+//	}
 func ConvertTo(obj interface{}) {
 	var ut expr.UserType
 	switch actual := eval.Current().(type) {
@@ -92,14 +91,14 @@ func ConvertTo(obj interface{}) {
 // algorithm used to match the external type fields to the design attributes is
 // as follows:
 //
-//    1. Look for an attribute with the same name as the field
-//    2. Look for an attribute with the same name as the field but with the
-//       first letter being lowercase
-//    3. Look for an attribute with a name corresponding to the snake_case
-//       version of the field name
+//  1. Look for an attribute with the same name as the field
+//  2. Look for an attribute with the same name as the field but with the
+//     first letter being lowercase
+//  3. Look for an attribute with a name corresponding to the snake_case
+//     version of the field name
 //
 // This algorithm does not apply if the attribute is equipped with the
-// "struct.field.external" meta. In this case the matching is done by
+// "struct:field:external" meta. In this case the matching is done by
 // looking up the field with a name corresponding to the value of the meta.
 // If the value of the meta is "-" the attribute isn't matched and no
 // conversion code is generated for it. In all other cases it is an error if no
@@ -109,8 +108,8 @@ func ConvertTo(obj interface{}) {
 // The following limitations apply on the external Go struct field types
 // recursively:
 //
-//    * struct fields must use pointers
-//    * pointers on slices or on maps are not supported
+//   - struct fields must use pointers
+//   - pointers on slices or on maps are not supported
 //
 // CreateFrom must appear in Type or ResultType.
 //
@@ -120,34 +119,33 @@ func ConvertTo(obj interface{}) {
 //
 // Service design:
 //
-//    var Bottle = Type("bottle", func() {
-//        Description("A bottle")
-//        CreateFrom(models.Bottle{})
-//        Attribute("rating", Int)
-//        Attribute("name", String, func() {
-//            // The "name" attribute is matched to the external
-//            // type "MyName" field.
-//            Meta("struct.field.external", "MyName")
-//        })
-//        Attribute("vineyard", String, func() {
-//            // The "vineyard" attribute is not initialized by the
-//            // generated constructor method.
-//            Meta("struct.field.external", "-")
-//        })
-//    })
+//	var Bottle = Type("bottle", func() {
+//	    Description("A bottle")
+//	    CreateFrom(models.Bottle{})
+//	    Attribute("rating", Int)
+//	    Attribute("name", String, func() {
+//	        // The "name" attribute is matched to the external
+//	        // type "MyName" field.
+//	        Meta("struct:field:external", "MyName")
+//	    })
+//	    Attribute("vineyard", String, func() {
+//	        // The "vineyard" attribute is not initialized by the
+//	        // generated constructor method.
+//	        Meta("struct:field:external", "-")
+//	    })
+//	})
 //
 // External (i.e. non design) package:
 //
-//    package model
-//
-//    type Bottle struct {
-//        Rating int
-//        // Mapped field
-//        MyName string
-//        // Additional fields are OK
-//        Description string
-//    }
+//	package model
 //
+//	type Bottle struct {
+//	    Rating int
+//	    // Mapped field
+//	    MyName string
+//	    // Additional fields are OK
+//	    Description string
+//	}
 func CreateFrom(obj interface{}) {
 	var ut expr.UserType
 	switch actual := eval.Current().(type) {
diff --git a/grpc/codegen/protobuf_transform.go b/grpc/codegen/protobuf_transform.go
index ae66ad86..f3f436af 100644
--- a/grpc/codegen/protobuf_transform.go
+++ b/grpc/codegen/protobuf_transform.go
@@ -114,6 +114,7 @@ func removeMeta(att *expr.AttributeExpr) {
 	_ = codegen.Walk(att, func(a *expr.AttributeExpr) error {
 		delete(a.Meta, "struct:field:name")
 		delete(a.Meta, "struct:field:external")
+		delete(a.Meta, "struct.field.external") // Deprecated syntax. Only present for backward compatibility.
 		return nil
 	})
 }


Test patch:
diff --git a/codegen/service/convert_test.go b/codegen/service/convert_test.go
index ab4d42c7..9a0f3e2b 100644
--- a/codegen/service/convert_test.go
+++ b/codegen/service/convert_test.go
@@ -310,11 +310,11 @@ func runDSL(t *testing.T, dsl func()) *expr.RootExpr {
 var obj = &expr.UserTypeExpr{
 	AttributeExpr: &expr.AttributeExpr{
 		Type: &expr.Object{
-			{"Foo", &expr.AttributeExpr{Type: expr.String}},
-			{"Bar", &expr.AttributeExpr{Type: expr.Int}},
-			{"Baz", &expr.AttributeExpr{Type: expr.Boolean}},
-			{"Goo", &expr.AttributeExpr{Type: expr.Float32}},
-			{"Goo2", &expr.AttributeExpr{Type: expr.UInt}},
+			{Name: "Foo", Attribute: &expr.AttributeExpr{Type: expr.String}},
+			{Name: "Bar", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+			{Name: "Baz", Attribute: &expr.AttributeExpr{Type: expr.Boolean}},
+			{Name: "Goo", Attribute: &expr.AttributeExpr{Type: expr.Float32}},
+			{Name: "Goo2", Attribute: &expr.AttributeExpr{Type: expr.UInt}},
 		},
 	},
 	TypeName: "objT",
@@ -323,9 +323,10 @@ var obj = &expr.UserTypeExpr{
 var objMapped = &expr.UserTypeExpr{
 	AttributeExpr: &expr.AttributeExpr{
 		Type: &expr.Object{
-			{"Foo", &expr.AttributeExpr{Type: expr.String}},
-			{"Bar", &expr.AttributeExpr{Type: expr.Int}},
-			{"mapped", &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct.field.external": []string{"Baz"}}}},
+			{Name: "Foo", Attribute: &expr.AttributeExpr{Type: expr.String}},
+			{Name: "Bar", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+			{Name: "mapped", Attribute: &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct:field:external": []string{"Baz"}}}},
+			{Name: "mapped (deprecated syntax)", Attribute: &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct.field.external": []string{"Baz"}}}},
 		},
 	},
 	TypeName: "objT",
@@ -334,9 +335,10 @@ var objMapped = &expr.UserTypeExpr{
 var objIgnored = &expr.UserTypeExpr{
 	AttributeExpr: &expr.AttributeExpr{
 		Type: &expr.Object{
-			{"Foo", &expr.AttributeExpr{Type: expr.String}},
-			{"Bar", &expr.AttributeExpr{Type: expr.Int}},
-			{"ignored", &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct.field.external": []string{"-"}}}},
+			{Name: "Foo", Attribute: &expr.AttributeExpr{Type: expr.String}},
+			{Name: "Bar", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+			{Name: "ignored", Attribute: &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct:field:external": []string{"-"}}}},
+			{Name: "ignored (deprecated syntax)", Attribute: &expr.AttributeExpr{Type: expr.Boolean, Meta: expr.MetaExpr{"struct.field.external": []string{"-"}}}},
 		},
 	},
 	TypeName: "objT",
@@ -346,8 +348,8 @@ func objRecursive() *expr.UserTypeExpr {
 	res := &expr.UserTypeExpr{
 		AttributeExpr: &expr.AttributeExpr{
 			Type: &expr.Object{
-				{"Foo", &expr.AttributeExpr{Type: expr.String}},
-				{"Bar", &expr.AttributeExpr{Type: expr.Int}},
+				{Name: "Foo", Attribute: &expr.AttributeExpr{Type: expr.String}},
+				{Name: "Bar", Attribute: &expr.AttributeExpr{Type: expr.Int}},
 			},
 		},
 		TypeName: "objRecursiveT",
diff --git a/codegen/transformer_test.go b/codegen/transformer_test.go
index 283ef2c0..1a40acf3 100644
--- a/codegen/transformer_test.go
+++ b/codegen/transformer_test.go
@@ -10,7 +10,7 @@ func TestIsPrimitivePointer(t *testing.T) {
 	newObj := func(fieldName string, fieldType expr.DataType, req bool) *expr.AttributeExpr {
 		attr := &expr.AttributeExpr{
 			Type: &expr.Object{
-				&expr.NamedAttributeExpr{fieldName, &expr.AttributeExpr{Type: fieldType}},
+				&expr.NamedAttributeExpr{Name: fieldName, Attribute: &expr.AttributeExpr{Type: fieldType}},
 			},
 		}
 		if req {
diff --git a/codegen/types_test.go b/codegen/types_test.go
index 97bba36b..d80638ac 100644
--- a/codegen/types_test.go
+++ b/codegen/types_test.go
@@ -17,14 +17,14 @@ func TestGoTypeDef(t *testing.T) {
 			}}
 		requiredObj = &expr.AttributeExpr{
 			Type: &expr.Object{
-				{"IntField", &expr.AttributeExpr{Type: expr.Int}},
-				{"StringField", &expr.AttributeExpr{Type: expr.String}},
+				{Name: "IntField", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				{Name: "StringField", Attribute: &expr.AttributeExpr{Type: expr.String}},
 			},
 			Validation: &expr.ValidationExpr{Required: []string{"IntField", "StringField"}}}
 		defaultObj = &expr.AttributeExpr{
 			Type: &expr.Object{
-				{"IntField", &expr.AttributeExpr{Type: expr.Int, DefaultValue: 1}},
-				{"StringField", &expr.AttributeExpr{Type: expr.String, DefaultValue: "foo"}},
+				{Name: "IntField", Attribute: &expr.AttributeExpr{Type: expr.Int, DefaultValue: 1}},
+				{Name: "StringField", Attribute: &expr.AttributeExpr{Type: expr.String, DefaultValue: "foo"}},
 			}}
 		ut                     = &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "UserType"}
 		rt                     = &expr.ResultTypeExpr{UserTypeExpr: &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "ResultType"}, Identifier: "application/vnd.goa.example", Views: nil}
@@ -38,13 +38,13 @@ func TestGoTypeDef(t *testing.T) {
 
 		mixedObj = &expr.AttributeExpr{
 			Type: &expr.Object{
-				{"IntField", &expr.AttributeExpr{Type: expr.Int}},
-				{"ArrayField", simpleArray},
-				{"MapField", simpleMap},
-				{"UserTypeField", userType},
-				{"MetaTypeField", &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithImportMetaType}},
-				{"QualifiedMetaTypeField", &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithRenameMetaType}},
-				{"StructPkgPath", &expr.AttributeExpr{Type: utPkgPathMeta}},
+				{Name: "IntField", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				{Name: "ArrayField", Attribute: simpleArray},
+				{Name: "MapField", Attribute: simpleMap},
+				{Name: "UserTypeField", Attribute: userType},
+				{Name: "MetaTypeField", Attribute: &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithImportMetaType}},
+				{Name: "QualifiedMetaTypeField", Attribute: &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithRenameMetaType}},
+				{Name: "StructPkgPath", Attribute: &expr.AttributeExpr{Type: utPkgPathMeta}},
 			},
 			Validation: &expr.ValidationExpr{Required: []string{"IntField", "ArrayField", "MapField", "UserTypeField", "MetaTypeField", "QualifiedMetaTypeField"}}}
 	)
diff --git a/dsl/validation_test.go b/dsl/validation_test.go
index d2875417..21209f9c 100644
--- a/dsl/validation_test.go
+++ b/dsl/validation_test.go
@@ -50,8 +50,8 @@ func TestRequired(t *testing.T) {
 		Type: &expr.UserTypeExpr{
 			AttributeExpr: &expr.AttributeExpr{
 				Type: &expr.Object{
-					{"foo", &expr.AttributeExpr{Type: String}},
-					{"bar", &expr.AttributeExpr{Type: String}},
+					{Name: "foo", Attribute: &expr.AttributeExpr{Type: String}},
+					{Name: "bar", Attribute: &expr.AttributeExpr{Type: String}},
 				},
 			},
 			TypeName: "Foo",
diff --git a/http/codegen/openapi/v3/types_test.go b/http/codegen/openapi/v3/types_test.go
index 3607d533..d9c97b54 100644
--- a/http/codegen/openapi/v3/types_test.go
+++ b/http/codegen/openapi/v3/types_test.go
@@ -311,7 +311,7 @@ func TestHashAttribute(t *testing.T) {
 
 func newObj(n string, t expr.DataType, req bool) *expr.AttributeExpr {
 	attr := &expr.AttributeExpr{
-		Type:       &expr.Object{{n, &expr.AttributeExpr{Type: t}}},
+		Type:       &expr.Object{{Name: n, Attribute: &expr.AttributeExpr{Type: t}}},
 		Validation: &expr.ValidationExpr{},
 	}
 	if req {
@@ -323,8 +323,8 @@ func newObj(n string, t expr.DataType, req bool) *expr.AttributeExpr {
 func newObj2(n, o string, t, u expr.DataType, reqs ...string) *expr.AttributeExpr {
 	attr := &expr.AttributeExpr{
 		Type: &expr.Object{
-			{n, &expr.AttributeExpr{Type: t}},
-			{o, &expr.AttributeExpr{Type: u}},
+			{Name: n, Attribute: &expr.AttributeExpr{Type: t}},
+			{Name: o, Attribute: &expr.AttributeExpr{Type: u}},
 		},
 		Validation: &expr.ValidationExpr{},
 	}