Repository: goadesign/goa
Pull request #3018 (2022-03-02 03:50:51)

Handle user types with attributes using types defined in custom packages


Patch:
diff --git a/codegen/import.go b/codegen/import.go
index a799112f..2eb1928c 100644
--- a/codegen/import.go
+++ b/codegen/import.go
@@ -2,6 +2,7 @@ package codegen
 
 import (
 	"fmt"
+	"path/filepath"
 	"strconv"
 
 	"goa.design/goa/v3/expr"
@@ -27,8 +28,9 @@ type (
 	Location struct {
 		// FilePath is the path to the file.
 		FilePath string
-		// ImportPath is the Go import path.
-		ImportPath string
+		// RelImportPath is the Go import path starting after the gen
+		// folder.
+		RelImportPath string
 	}
 )
 
@@ -72,6 +74,27 @@ func (s *ImportSpec) Code() string {
 	return fmt.Sprintf(`"%s"`, s.Path)
 }
 
+// UserTypeLocation returns the location of the user type if set via the
+// attr:pkg:path metadata, nil otherwise..
+func UserTypeLocation(ut expr.UserType) *Location {
+	p, ok := ut.Attribute().Meta.Last("struct:pkg:path")
+	if !ok || p == "" {
+		return nil
+	}
+	return &Location{
+		FilePath:      filepath.Join(filepath.FromSlash(p), SnakeCase(ut.Name())+".go"),
+		RelImportPath: p,
+	}
+}
+
+// PackageName returns the package name of the given location.
+func (loc *Location) PackageName() string {
+	if loc == nil {
+		return ""
+	}
+	return Goify(filepath.Base(loc.RelImportPath), false)
+}
+
 // GetMetaType retrieves the type and package defined by the struct:field:type
 // metadata if any.
 func GetMetaType(att *expr.AttributeExpr) (typeName string, importS *ImportSpec) {
diff --git a/codegen/scope.go b/codegen/scope.go
index 0795a722..a1cc7133 100644
--- a/codegen/scope.go
+++ b/codegen/scope.go
@@ -149,7 +149,11 @@ func (s *NameScope) GoTypeDef(att *expr.AttributeExpr, ptr, useDefault bool) str
 		ss = append(ss, "}")
 		return strings.Join(ss, "\n")
 	case expr.UserType:
-		return s.GoTypeName(att)
+		var prefix string
+		if loc := UserTypeLocation(actual); loc != nil {
+			prefix = loc.PackageName() + "."
+		}
+		return prefix + s.GoTypeName(att)
 	default:
 		panic(fmt.Sprintf("unknown data type %T", actual)) // bug
 	}
diff --git a/codegen/service/service.go b/codegen/service/service.go
index d47dd54f..c6d17b77 100644
--- a/codegen/service/service.go
+++ b/codegen/service/service.go
@@ -218,7 +218,7 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s
 		if len(secs) == 0 {
 			continue
 		}
-		h := codegen.Header("User types", packageFromPath(p), nil)
+		h := codegen.Header("User types", codegen.Goify(filepath.Base(p), false), nil)
 		sections := append([]*codegen.SectionTemplate{h}, secs...)
 		files = append(files, &codegen.File{Path: filepath.Join(codegen.Gendir, p), SectionTemplates: sections})
 	}
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index ec2af82c..904ec152 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -3,7 +3,6 @@ package service
 import (
 	"bytes"
 	"fmt"
-	"path/filepath"
 	"strings"
 	"text/template"
 
@@ -444,14 +443,7 @@ func (d *Data) initUserTypeImports(genpkg string) {
 		if loc == nil {
 			return
 		}
-		dir := filepath.Dir(loc.FilePath)
-		loc.ImportPath = genpkg + "/" + dir
-		base := filepath.Base(dir)
-		var pkg string
-		if p := codegen.Goify(base, false); p != base {
-			pkg = p
-		}
-		importsByPath[loc.FilePath] = &codegen.ImportSpec{Name: pkg, Path: loc.ImportPath}
+		importsByPath[loc.FilePath] = &codegen.ImportSpec{Name: loc.PackageName(), Path: genpkg + "/" + loc.RelImportPath}
 	}
 
 	for _, m := range d.Methods {
@@ -733,17 +725,13 @@ func collectTypes(at *expr.AttributeExpr, scope *codegen.NameScope, seen map[str
 		if _, ok := seen[dt.ID()]; ok {
 			return nil
 		}
-		var loc *codegen.Location
-		if p := genPath(dt); p != "" {
-			loc = &codegen.Location{FilePath: p}
-		}
 		data = append(data, &UserTypeData{
 			Name:        dt.Name(),
 			VarName:     scope.GoTypeName(at),
 			Description: dt.Attribute().Description,
 			Def:         scope.GoTypeDef(dt.Attribute(), false, true),
 			Ref:         scope.GoTypeRef(at),
-			Loc:         loc,
+			Loc:         codegen.UserTypeLocation(dt),
 			Type:        dt,
 		})
 		seen[dt.ID()] = struct{}{}
@@ -768,7 +756,7 @@ func buildErrorInitData(er *expr.ErrorExpr, scope *codegen.NameScope) *ErrorInit
 	_, fault := er.AttributeExpr.Meta["goa:error:fault"]
 	var pkg string
 	if ut, ok := er.AttributeExpr.Type.(expr.UserType); ok {
-		pkg = packageFromPath(genPath(ut))
+		pkg = codegen.UserTypeLocation(ut).PackageName()
 	}
 	return &ErrorInitData{
 		Name:        fmt.Sprintf("Make%s", codegen.Goify(er.Name, true)),
@@ -789,13 +777,13 @@ func buildMethodData(m *expr.MethodExpr, svcPkgName string, service *expr.Servic
 		vname       string
 		desc        string
 		payloadName string
-		payloadPath string
+		payloadLoc  *codegen.Location
 		payloadDef  string
 		payloadRef  string
 		payloadDesc string
 		payloadEx   interface{}
 		rname       string
-		resultPath  string
+		resultLoc   *codegen.Location
 		resultDef   string
 		resultRef   string
 		resultDesc  string
@@ -813,9 +801,9 @@ func buildMethodData(m *expr.MethodExpr, svcPkgName string, service *expr.Servic
 		payloadName = scope.GoTypeName(m.Payload)
 		if dt, ok := m.Payload.Type.(expr.UserType); ok {
 			payloadDef = scope.GoTypeDef(dt.Attribute(), false, true)
-			payloadPath = genPath(dt)
+			payloadLoc = codegen.UserTypeLocation(dt)
 		}
-		payloadRef = scope.GoFullTypeRef(m.Payload, packageFromPath(payloadPath))
+		payloadRef = scope.GoFullTypeRef(m.Payload, payloadLoc.PackageName())
 		payloadDesc = m.Payload.Description
 		if payloadDesc == "" {
 			payloadDesc = fmt.Sprintf("%s is the payload type of the %s service %s method.",
@@ -827,9 +815,9 @@ func buildMethodData(m *expr.MethodExpr, svcPkgName string, service *expr.Servic
 		rname = scope.GoTypeName(m.Result)
 		if dt, ok := m.Result.Type.(expr.UserType); ok {
 			resultDef = scope.GoTypeDef(dt.Attribute(), false, true)
-			resultPath = genPath(dt)
+			resultLoc = codegen.UserTypeLocation(dt)
 		}
-		resultRef = scope.GoFullTypeRef(m.Result, packageFromPath(resultPath))
+		resultRef = scope.GoFullTypeRef(m.Result, resultLoc.PackageName())
 		resultDesc = m.Result.Description
 		if resultDesc == "" {
 			resultDesc = fmt.Sprintf("%s is the result type of the %s service %s method.",
@@ -856,13 +844,6 @@ func buildMethodData(m *expr.MethodExpr, svcPkgName string, service *expr.Servic
 	if httpSvc := expr.Root.HTTPService(m.Service.Name); httpSvc != nil {
 		httpMet = httpSvc.Endpoint(m.Name)
 	}
-	var payloadLoc, resultLoc *codegen.Location
-	if payloadPath != "" {
-		payloadLoc = &codegen.Location{FilePath: payloadPath}
-	}
-	if resultPath != "" {
-		resultLoc = &codegen.Location{FilePath: resultPath}
-	}
 	data := &MethodData{
 		Name:                         m.Name,
 		VarName:                      vname,
@@ -1679,23 +1660,6 @@ func buildConstructorCode(src, tgt *expr.AttributeExpr, sourceVar, targetVar str
 	return buf.String(), helpers
 }
 
-// genPath returns the path to the generated file that declares the Go struct
-// corresponding to the given user type if set via Meta, empty string otherwise.
-func genPath(ut expr.UserType) string {
-	if p, ok := ut.Attribute().Meta.Last("struct:pkg:path"); ok && p != "" {
-		return filepath.Join(filepath.FromSlash(p), codegen.SnakeCase(ut.Name())+".go")
-	}
-	return ""
-}
-
-// packageFromPath returns the package name for the given path.
-func packageFromPath(path string) string {
-	if path == "" {
-		return ""
-	}
-	return codegen.Goify(filepath.Base(filepath.Dir(path)), false)
-}
-
 // walkViewAttrs iterates through the attributes in att that are found in the
 // given view and executes the walker function.
 func walkViewAttrs(obj *expr.Object, view *expr.ViewExpr, walker func(name string, attr, vatt *expr.AttributeExpr)) {
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index 432217c9..482dda0b 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -2,7 +2,6 @@ package codegen
 
 import (
 	"fmt"
-	"path"
 
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/codegen/service"
@@ -1280,7 +1279,7 @@ func pkgWithDefault(loc *codegen.Location, def string) string {
 	if loc == nil {
 		return def
 	}
-	return codegen.Goify(path.Base(loc.ImportPath), false)
+	return loc.PackageName()
 }
 
 // getPrimitive returns the primitive expression if the given expression is an alias to one
diff --git a/http/codegen/client.go b/http/codegen/client.go
index fe068e15..9f408dd9 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -2,7 +2,6 @@ package codegen
 
 import (
 	"fmt"
-	"path"
 	"path/filepath"
 
 	"goa.design/goa/v3/codegen"
@@ -240,14 +239,14 @@ func isBearer(schemes []*service.SchemeData) bool {
 
 func requestStructPkg(m *service.MethodData, def string) string {
 	if m.PayloadLoc != nil {
-		return codegen.Goify(path.Base(m.PayloadLoc.ImportPath), false)
+		return m.PayloadLoc.PackageName()
 	}
 	return def
 }
 
 func responseStructPkg(m *service.MethodData, def string) string {
 	if m.ResultLoc != nil {
-		return codegen.Goify(path.Base(m.ResultLoc.ImportPath), false)
+		return m.ResultLoc.PackageName()
 	}
 	return def
 }
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index f084c22f..ccf1cfba 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -2637,7 +2637,7 @@ func pkgWithDefault(loc *codegen.Location, def string) string {
 	if loc == nil {
 		return def
 	}
-	return codegen.Goify(path.Base(loc.ImportPath), false)
+	return loc.PackageName()
 }
 
 // unmarshal initializes a data structure defined by target type from a data


Test patch:
diff --git a/codegen/service/testdata/service_code.go b/codegen/service/testdata/service_code.go
index 57126d5d..1a85300f 100644
--- a/codegen/service/testdata/service_code.go
+++ b/codegen/service/testdata/service_code.go
@@ -2399,6 +2399,8 @@ type Service interface {
 	A(context.Context, *bar.Bar) (res *bar.Bar, err error)
 	// B implements B.
 	B(context.Context, *baz.Baz) (res *baz.Baz, err error)
+	// EnvelopedB implements EnvelopedB.
+	EnvelopedB(context.Context, *EnvelopedBPayload) (res *EnvelopedBResult, err error)
 }
 
 // ServiceName is the name of the service as defined in the design. This is the
@@ -2409,7 +2411,19 @@ const ServiceName = "MultiplePkgPathMethod"
 // MethodNames lists the service method names as defined in the design. These
 // are the same values that are set in the endpoint request contexts under the
 // MethodKey key.
-var MethodNames = [2]string{"A", "B"}
+var MethodNames = [3]string{"A", "B", "EnvelopedB"}
+
+// EnvelopedBPayload is the payload type of the MultiplePkgPathMethod service
+// EnvelopedB method.
+type EnvelopedBPayload struct {
+	Baz *baz.Baz
+}
+
+// EnvelopedBResult is the result type of the MultiplePkgPathMethod service
+// EnvelopedB method.
+type EnvelopedBResult struct {
+	Baz *baz.Baz
+}
 `
 
 const PkgPathFoo = `// Foo is the payload type of the PkgPathMethod service A method.
diff --git a/codegen/service/testdata/service_dsls.go b/codegen/service/testdata/service_dsls.go
index 8aaf48ce..fab2ea78 100644
--- a/codegen/service/testdata/service_dsls.go
+++ b/codegen/service/testdata/service_dsls.go
@@ -647,6 +647,15 @@ var PkgPathMultipleDSL = func() {
 			Payload(Baz)
 			Result(Baz)
 		})
+
+		Method("EnvelopedB", func() {
+			Payload(func() {
+				Attribute("Baz", Baz)
+			})
+			Result(func() {
+				Attribute("Baz", Baz)
+			})
+		})
 	})
 }
 
diff --git a/http/codegen/server_encode_test.go b/http/codegen/server_encode_test.go
index 18457b76..dab3f34b 100644
--- a/http/codegen/server_encode_test.go
+++ b/http/codegen/server_encode_test.go
@@ -80,6 +80,9 @@ func TestEncode(t *testing.T) {
 
 		{"empty-server-response", testdata.EmptyServerResponseDSL, testdata.EmptyServerResponseEncodeCode},
 		{"empty-server-response-with-tags", testdata.EmptyServerResponseWithTagsDSL, testdata.EmptyServerResponseWithTagsEncodeCode},
+
+		{"result-with-custom-pkg-type", testdata.ResultWithCustomPkgTypeDSL, testdata.ResultWithCustomPkgTypeEncodeCode},
+		{"result-with-embedded-custom-pkg-type", testdata.ResultWithEmbeddedCustomPkgTypeDSL, testdata.ResultWithEmbeddedCustomPkgTypeEncodeCode},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/http/codegen/testdata/result_dsls.go b/http/codegen/testdata/result_dsls.go
index ecb153f9..2b7e9337 100644
--- a/http/codegen/testdata/result_dsls.go
+++ b/http/codegen/testdata/result_dsls.go
@@ -744,6 +744,46 @@ var ResultWithResultCollectionDSL = func() {
 	})
 }
 
+var ResultWithCustomPkgTypeDSL = func() {
+	var Foo = Type("Foo", func() {
+		Meta("struct:pkg:path", "foo")
+		Attribute("bar", String)
+	})
+
+	Service("ServiceResultWithCustomPkgTypeDSL", func() {
+		Method("MethodResultWithCustomPkgTypeDSL", func() {
+			Payload(Foo)
+			Result(Foo)
+
+			HTTP(func() {
+				GET("/")
+			})
+		})
+	})
+}
+
+var ResultWithEmbeddedCustomPkgTypeDSL = func() {
+	var Foo = Type("Foo", func() {
+		Meta("struct:pkg:path", "foo")
+		Attribute("bar", String)
+	})
+
+	var ContainedFoo = Type("ContainedFoo", func() {
+		Attribute("Foo", Foo)
+	})
+
+	Service("ServiceResultWithEmbeddedCustomPkgTypeDSL", func() {
+		Method("MethodResultWithEmbeddedCustomPkgTypeDSL", func() {
+			Payload(ContainedFoo)
+			Result(ContainedFoo)
+
+			HTTP(func() {
+				GET("/")
+			})
+		})
+	})
+}
+
 var EmptyErrorResponseBodyDSL = func() {
 	Service("ServiceEmptyErrorResponseBody", func() {
 		Method("MethodEmptyErrorResponseBody", func() {
diff --git a/http/codegen/testdata/result_encode_functions.go b/http/codegen/testdata/result_encode_functions.go
index 14f13b44..331f9844 100644
--- a/http/codegen/testdata/result_encode_functions.go
+++ b/http/codegen/testdata/result_encode_functions.go
@@ -1022,3 +1022,31 @@ func EncodeMethodEmptyServerResponseWithTagsResponse(encoder func(context.Contex
 	}
 }
 `
+
+var ResultWithCustomPkgTypeEncodeCode = `// EncodeMethodResultWithCustomPkgTypeDSLResponse returns an encoder for
+// responses returned by the ServiceResultWithCustomPkgTypeDSL
+// MethodResultWithCustomPkgTypeDSL endpoint.
+func EncodeMethodResultWithCustomPkgTypeDSLResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, interface{}) error {
+	return func(ctx context.Context, w http.ResponseWriter, v interface{}) error {
+		res, _ := v.(*foo.Foo)
+		enc := encoder(ctx, w)
+		body := NewMethodResultWithCustomPkgTypeDSLResponseBody(res)
+		w.WriteHeader(http.StatusOK)
+		return enc.Encode(body)
+	}
+}
+`
+
+var ResultWithEmbeddedCustomPkgTypeEncodeCode = `// EncodeMethodResultWithEmbeddedCustomPkgTypeDSLResponse returns an encoder
+// for responses returned by the ServiceResultWithEmbeddedCustomPkgTypeDSL
+// MethodResultWithEmbeddedCustomPkgTypeDSL endpoint.
+func EncodeMethodResultWithEmbeddedCustomPkgTypeDSLResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, interface{}) error {
+	return func(ctx context.Context, w http.ResponseWriter, v interface{}) error {
+		res, _ := v.(*serviceresultwithembeddedcustompkgtypedsl.ContainedFoo)
+		enc := encoder(ctx, w)
+		body := NewMethodResultWithEmbeddedCustomPkgTypeDSLResponseBody(res)
+		w.WriteHeader(http.StatusOK)
+		return enc.Encode(body)
+	}
+}
+`