Skip to content

Commit 802c7af

Browse files
Danlockraphael
authored andcommitted
goadesign#2013 struct:field:type implementation (goadesign#2053)
* Added support for struct:field:type, added new functions for adding imports to codegen.File and added a new parameter to struct:field:type that allows qualified imports. Meta imports are available for method payloads to use as well * added comments, maintained backward compat with AddImport, unexported GetMetaType * appeased linter, added tests to to types_test.go, clarified pointer caveat with struct:field:type * aligned comments * fix infinite recursion
1 parent 477ff16 commit 802c7af

File tree

9 files changed

+182
-13
lines changed

9 files changed

+182
-13
lines changed

codegen/generator/example.go

+7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
5959
files = append(files, fs...)
6060
}
6161
}
62+
for _, f := range files {
63+
if len(f.SectionTemplates) > 0 {
64+
for _, s := range r.Services {
65+
service.AddServiceDataMetaTypeImports(f.SectionTemplates[0], s)
66+
}
67+
}
68+
}
6269
}
6370
return files, nil
6471
}

codegen/generator/service.go

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ func Service(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
2626
if f := service.ViewsFile(genpkg, s); f != nil {
2727
files = append(files, f)
2828
}
29+
for _, f := range files {
30+
if len(f.SectionTemplates) > 0 {
31+
service.AddServiceDataMetaTypeImports(f.SectionTemplates[0], s)
32+
}
33+
}
2934
f, err := service.ConvertFile(r, s)
3035
if err != nil {
3136
return nil, err

codegen/generator/transport.go

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"goa.design/goa/codegen"
7+
"goa.design/goa/codegen/service"
78
"goa.design/goa/eval"
89
"goa.design/goa/expr"
910
grpccodegen "goa.design/goa/grpc/codegen"
@@ -36,6 +37,14 @@ func Transport(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
3637
files = append(files, grpccodegen.ServerTypeFiles(genpkg, r)...)
3738
files = append(files, grpccodegen.ClientTypeFiles(genpkg, r)...)
3839
files = append(files, grpccodegen.ClientCLIFiles(genpkg, r)...)
40+
41+
for _, f := range files {
42+
if len(f.SectionTemplates) > 0 {
43+
for _, s := range r.Services {
44+
service.AddServiceDataMetaTypeImports(f.SectionTemplates[0], s)
45+
}
46+
}
47+
}
3948
}
4049
if len(files) == 0 {
4150
return nil, fmt.Errorf("transport: no HTTP/gRPC design found")

codegen/header.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@ func Header(title, pack string, imports []*ImportSpec) *SectionTemplate {
1818
}
1919
}
2020

21-
// AddImport adds an import to a section template that was generated with
21+
// AddImport adds imports to a section template that was generated with
2222
// Header.
23-
func AddImport(section *SectionTemplate, imprt *ImportSpec) {
23+
func AddImport(section *SectionTemplate, imprts ...*ImportSpec) {
24+
if len(imprts) == 0 {
25+
return
26+
}
2427
var specs []*ImportSpec
2528
if data, ok := section.Data.(map[string]interface{}); ok {
2629
if imports, ok := data["Imports"]; ok {
2730
specs = imports.([]*ImportSpec)
2831
}
29-
data["Imports"] = append(specs, imprt)
32+
data["Imports"] = append(specs, imprts...)
3033
}
3134
}
3235

codegen/import.go

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package codegen
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
6+
"goa.design/goa/expr"
7+
)
48

59
type (
610
// ImportSpec defines a generated import statement.
@@ -29,3 +33,96 @@ func (s *ImportSpec) Code() string {
2933
}
3034
return fmt.Sprintf(`"%s"`, s.Path)
3135
}
36+
37+
// getMetaTypeInfo gets type and import info from an attribute's metadata. struct:field:type can have 3 arguments,
38+
// first being the go type name, second being import path,
39+
// and third being the name of a qualified import, in case of name collisions.
40+
func getMetaTypeInfo(att *expr.AttributeExpr) (typeName string, importS *ImportSpec) {
41+
if att == nil {
42+
return typeName, importS
43+
}
44+
if args, ok := att.Meta["struct:field:type"]; ok {
45+
if len(args) > 0 {
46+
typeName = args[0]
47+
}
48+
if len(args) > 1 {
49+
importS = &ImportSpec{Path: args[1]}
50+
}
51+
if len(args) > 2 {
52+
importS.Name = args[2]
53+
}
54+
}
55+
return typeName, importS
56+
}
57+
58+
// GetMetaTypeImports parses the attribute for all user defined imports
59+
func GetMetaTypeImports(att *expr.AttributeExpr) []*ImportSpec {
60+
return safelyGetMetaTypeImports(att, nil)
61+
}
62+
63+
// safelyGetMetaTypeImports parses attributes while keeping track of previous usertypes to avoid infinite recursion
64+
func safelyGetMetaTypeImports(att *expr.AttributeExpr, seen map[string]struct{}) []*ImportSpec {
65+
if att == nil {
66+
return nil
67+
}
68+
if seen == nil {
69+
seen = make(map[string]struct{})
70+
}
71+
uniqueImports := make(map[ImportSpec]struct{})
72+
imports := make([]*ImportSpec, 0)
73+
74+
switch t := att.Type.(type) {
75+
case expr.UserType:
76+
if _, wasSeen := seen[t.ID()]; wasSeen {
77+
return imports
78+
}
79+
seen[t.ID()] = struct{}{}
80+
for _, im := range safelyGetMetaTypeImports(t.Attribute(), seen) {
81+
if im != nil {
82+
uniqueImports[*im] = struct{}{}
83+
}
84+
}
85+
case *expr.Array:
86+
_, im := getMetaTypeInfo(t.ElemType)
87+
if im != nil {
88+
uniqueImports[*im] = struct{}{}
89+
}
90+
case *expr.Map:
91+
_, im := getMetaTypeInfo(t.ElemType)
92+
if im != nil {
93+
uniqueImports[*im] = struct{}{}
94+
}
95+
_, im = getMetaTypeInfo(t.KeyType)
96+
if im != nil {
97+
uniqueImports[*im] = struct{}{}
98+
}
99+
case *expr.Object:
100+
for _, key := range *t {
101+
if key != nil {
102+
_, im := getMetaTypeInfo(key.Attribute)
103+
if im != nil {
104+
uniqueImports[*im] = struct{}{}
105+
}
106+
}
107+
}
108+
}
109+
_, im := getMetaTypeInfo(att)
110+
if im != nil {
111+
uniqueImports[*im] = struct{}{}
112+
}
113+
for imp := range uniqueImports {
114+
// Copy loop variable into body so next iteration doesnt overwrite its address https://stackoverflow.com/questions/27610039/golang-appending-leaves-only-last-element
115+
copy := imp
116+
imports = append(imports, &copy)
117+
}
118+
return imports
119+
}
120+
121+
// AddServiceMetaTypeImports adds meta type imports for each method of the service expr
122+
func AddServiceMetaTypeImports(header *SectionTemplate, svc *expr.ServiceExpr) {
123+
for _, m := range svc.Methods {
124+
AddImport(header, GetMetaTypeImports(m.Payload)...)
125+
AddImport(header, GetMetaTypeImports(m.StreamingPayload)...)
126+
AddImport(header, GetMetaTypeImports(m.Result)...)
127+
}
128+
}

codegen/scope.go

+6
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ func (s *NameScope) Name(name string) string {
9898
func (s *NameScope) GoTypeDef(att *expr.AttributeExpr, ptr, useDefault bool) string {
9999
switch actual := att.Type.(type) {
100100
case expr.Primitive:
101+
if t, _ := getMetaTypeInfo(att); t != "" {
102+
return t
103+
}
101104
return GoNativeTypeName(actual)
102105
case *expr.Array:
103106
d := s.GoTypeDef(actual.ElemType, ptr, useDefault)
@@ -186,6 +189,9 @@ func (s *NameScope) GoTypeName(att *expr.AttributeExpr) string {
186189
func (s *NameScope) GoFullTypeName(att *expr.AttributeExpr, pkg string) string {
187190
switch actual := att.Type.(type) {
188191
case expr.Primitive:
192+
if t, _ := getMetaTypeInfo(att); t != "" {
193+
return t
194+
}
189195
return GoNativeTypeName(actual)
190196
case *expr.Array:
191197
return "[]" + s.GoFullTypeRef(actual.ElemType, pkg)

codegen/service/service.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ func File(genpkg string, service *expr.ServiceExpr) *codegen.File {
6666
}
6767
}
6868
}
69-
7069
for _, ut := range svc.userTypes {
7170
if _, ok := seen[ut.Name]; !ok {
7271
sections = append(sections, &codegen.SectionTemplate{
@@ -152,6 +151,24 @@ func File(genpkg string, service *expr.ServiceExpr) *codegen.File {
152151
return &codegen.File{Path: path, SectionTemplates: sections}
153152
}
154153

154+
// AddServiceDataMetaTypeImports Adds all imports defined by struct:field:type from the service expr and the service data
155+
func AddServiceDataMetaTypeImports(header *codegen.SectionTemplate, serviceE *expr.ServiceExpr) {
156+
codegen.AddServiceMetaTypeImports(header, serviceE)
157+
svc := Services.Get(serviceE.Name)
158+
for _, ut := range svc.userTypes {
159+
codegen.AddImport(header, codegen.GetMetaTypeImports(ut.Type.Attribute())...)
160+
}
161+
for _, et := range svc.errorTypes {
162+
codegen.AddImport(header, codegen.GetMetaTypeImports(et.Type.Attribute())...)
163+
}
164+
for _, t := range svc.viewedResultTypes {
165+
codegen.AddImport(header, codegen.GetMetaTypeImports(t.Type.Attribute())...)
166+
}
167+
for _, t := range svc.projectedTypes {
168+
codegen.AddImport(header, codegen.GetMetaTypeImports(t.Type.Attribute())...)
169+
}
170+
}
171+
155172
func errorName(et *UserTypeData) string {
156173
obj := expr.AsObject(et.Type)
157174
if obj != nil {

codegen/types_test.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,23 @@ func TestGoTypeDef(t *testing.T) {
2626
{"IntField", &expr.AttributeExpr{Type: expr.Int, DefaultValue: 1}},
2727
{"StringField", &expr.AttributeExpr{Type: expr.String, DefaultValue: "foo"}},
2828
}}
29-
ut = &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "UserType"}
30-
rt = &expr.ResultTypeExpr{UserTypeExpr: &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "ResultType"}, Identifier: "application/vnd.goa.example", Views: nil}
31-
userType = &expr.AttributeExpr{Type: ut}
32-
resultType = &expr.AttributeExpr{Type: rt}
33-
mixedObj = &expr.AttributeExpr{
29+
ut = &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "UserType"}
30+
rt = &expr.ResultTypeExpr{UserTypeExpr: &expr.UserTypeExpr{AttributeExpr: &expr.AttributeExpr{Type: expr.Boolean}, TypeName: "ResultType"}, Identifier: "application/vnd.goa.example", Views: nil}
31+
userType = &expr.AttributeExpr{Type: ut}
32+
resultType = &expr.AttributeExpr{Type: rt}
33+
stringMetaType = expr.MetaExpr{"struct:field:type": []string{"string"}}
34+
jsonWithImportMetaType = expr.MetaExpr{"struct:field:type": []string{"json.RawMessage", "encoding/json"}}
35+
jsonWithRenameMetaType = expr.MetaExpr{"struct:field:type": []string{"jason.RawMessage", "encoding/json", "jason"}}
36+
mixedObj = &expr.AttributeExpr{
3437
Type: &expr.Object{
3538
{"IntField", &expr.AttributeExpr{Type: expr.Int}},
3639
{"ArrayField", simpleArray},
3740
{"MapField", simpleMap},
3841
{"UserTypeField", userType},
42+
{"MetaTypeField", &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithImportMetaType}},
43+
{"QualifiedMetaTypeField", &expr.AttributeExpr{Type: expr.Int, Meta: jsonWithRenameMetaType}},
3944
},
40-
Validation: &expr.ValidationExpr{Required: []string{"IntField", "ArrayField", "MapField", "UserTypeField"}}}
45+
Validation: &expr.ValidationExpr{Required: []string{"IntField", "ArrayField", "MapField", "UserTypeField", "MetaTypeField", "QualifiedMetaTypeField"}}}
4146
)
4247
cases := map[string]struct {
4348
att *expr.AttributeExpr
@@ -66,8 +71,12 @@ func TestGoTypeDef(t *testing.T) {
6671
"Object": {requiredObj, false, true, "struct {\n\tIntField int\n\tStringField string\n}"},
6772
"ObjDefault": {defaultObj, false, true, "struct {\n\tIntField int\n\tStringField string\n}"},
6873
"ObjDefaultNoDef": {defaultObj, false, false, "struct {\n\tIntField *int\n\tStringField *string\n}"},
69-
"ObjMixed": {mixedObj, false, true, "struct {\n\tIntField int\n\tArrayField []bool\n\tMapField map[int]string\n\tUserTypeField UserType\n}"},
70-
"ObjMixedPointer": {mixedObj, true, true, "struct {\n\tIntField *int\n\tArrayField []bool\n\tMapField map[int]string\n\tUserTypeField *UserType\n}"},
74+
"ObjMixed": {mixedObj, false, true, "struct {\n\tIntField int\n\tArrayField []bool\n\tMapField map[int]string\n\tUserTypeField UserType\n\tMetaTypeField json.RawMessage\n\tQualifiedMetaTypeField jason.RawMessage\n}"},
75+
"ObjMixedPointer": {mixedObj, true, true, "struct {\n\tIntField *int\n\tArrayField []bool\n\tMapField map[int]string\n\tUserTypeField *UserType\n\tMetaTypeField *json.RawMessage\n\tQualifiedMetaTypeField *jason.RawMessage\n}"},
76+
77+
"MetaTypeSameAsDesign": {&expr.AttributeExpr{Type: expr.String, Meta: stringMetaType}, false, true, "string"},
78+
"MetaTypeOverrideDesign": {&expr.AttributeExpr{Type: expr.String, Meta: jsonWithImportMetaType}, false, true, "json.RawMessage"},
79+
"MetaTypeOverrideDesignWithQualifiedImport": {&expr.AttributeExpr{Type: expr.String, Meta: jsonWithRenameMetaType}, false, true, "jason.RawMessage"},
7180
}
7281

7382
for k, tc := range cases {

dsl/meta.go

+16
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ import (
6363
// })
6464
// })
6565
//
66+
// - "struct:field:type" overrides the Go struct field type specified in the design, with one caveat;
67+
// if the type would have been a pointer (such as its not Required) the new type will also be a pointer.
68+
// Applicable to attributes only. The import path of the type should be passed in as the second parameter, if needed.
69+
// If the default imported package name conflicts with another, you can override that as well with the third parameter.
70+
//
71+
// var MyType = Type("BigOleMessage", func() {
72+
// Attribute("type", String, "Type of big payload")
73+
// Attribute("bigPayload", String, "Don't parse it if you don't have to",func() {
74+
// Meta("struct:field:type","json.RawMessage","encoding/json")
75+
// })
76+
// Attribute("id", String, func() {
77+
// Meta("struct:field:type","bison.ObjectId", "github.com/globalsign/mgo/bson", "bison")
78+
// })
79+
// })
80+
//
81+
//
6682
// - "struct:tag:xxx" sets a generated Go struct field tag and overrides tags
6783
// that goa would otherwise set. If the metadata value is a slice then the
6884
// strings are joined with the space character as separator. Applicable to

0 commit comments

Comments
 (0)