diff --git a/expr/result_type.go b/expr/result_type.go index 8a098a77f1..1d637e08bd 100644 --- a/expr/result_type.go +++ b/expr/result_type.go @@ -363,7 +363,7 @@ func projectCollection(rt *ResultTypeExpr, view string, seen map[string]*Attribu } func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, seen map[string]*AttributeExpr) (*AttributeExpr, error) { - if att, ok := seen[hashAttrAndView(at, view)]; ok { + if att, ok := seen[hashAttrViewField(at, view, vat.Name)]; ok { return att, nil } at = DupAtt(at) @@ -378,7 +378,7 @@ func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, s view = DefaultView } } - seen[hashAttrAndView(at, view)] = at + seen[hashAttrViewField(at, view, vat.Name)] = at pr, err := project(rt, view, seen) if err != nil { return nil, fmt.Errorf("view %#v on field %#v cannot be computed: %w", view, vat.Name, err) @@ -388,7 +388,7 @@ func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, s } if _, ok := at.Type.(*UserTypeExpr); ok { - seen[hashAttrAndView(at, view)] = at + seen[hashAttrViewField(at, view, vat.Name)] = at } if obj := AsObject(at.Type); obj != nil { @@ -462,3 +462,9 @@ func (v *ViewExpr) EvalName() string { func hashAttrAndView(att *AttributeExpr, view string) string { return Hash(att.Type, false, false, false) + "::" + view } + +// hashAttrViewField computes the projection cache key for an attribute, view, +// and field name. +func hashAttrViewField(att *AttributeExpr, view, field string) string { + return hashAttrAndView(att, view) + "::" + field +} diff --git a/http/codegen/server_types_test.go b/http/codegen/server_types_test.go index b6bc66c726..aa7fbf06b7 100644 --- a/http/codegen/server_types_test.go +++ b/http/codegen/server_types_test.go @@ -25,6 +25,8 @@ func TestServerTypes(t *testing.T) { {"server-result-type-validate", testdata.ResultTypeValidateDSL}, {"server-with-result-collection", testdata.ResultWithResultCollectionDSL}, {"server-with-result-view", testdata.ResultWithResultViewDSL}, + {"server-with-result-sibling-user-type-fields", testdata.ResultTypeSiblingUserTypeFieldsDSL}, + {"server-with-result-collection-sibling-user-type-fields", testdata.ResultTypeCollectionSiblingUserTypeFieldsDSL}, {"server-empty-error-response-body", testdata.EmptyErrorResponseBodyDSL}, {"server-with-error-custom-pkg", testdata.WithErrorCustomPkgDSL}, {"server-body-custom-name", testdata.PayloadBodyCustomNameDSL}, diff --git a/http/codegen/testdata/golden/server_types_server-with-result-collection-sibling-user-type-fields.go.golden b/http/codegen/testdata/golden/server_types_server-with-result-collection-sibling-user-type-fields.go.golden new file mode 100644 index 0000000000..288dad2fad --- /dev/null +++ b/http/codegen/testdata/golden/server_types_server-with-result-collection-sibling-user-type-fields.go.golden @@ -0,0 +1,33 @@ +// ResulttypesiblingcollectionResponseCollection is the type of the +// "ServiceResultCollectionUserTypeSibling" service +// "MethodResultCollectionUserTypeSibling" endpoint HTTP response body. +type ResulttypesiblingcollectionResponseCollection []*ResulttypesiblingcollectionResponse + +// ResulttypesiblingcollectionResponse is used to define fields on response +// body types. +type ResulttypesiblingcollectionResponse struct { + // Attribute A + A *UserTypeResponse `json:"a"` + // Attribute B + B *UserTypeResponse `json:"b"` +} + +// UserTypeResponse is used to define fields on response body types. +type UserTypeResponse struct { + U *int `form:"u,omitempty" json:"u,omitempty" xml:"u,omitempty"` +} + +// NewResulttypesiblingcollectionResponseCollection builds the HTTP response +// body from the result of the "MethodResultCollectionUserTypeSibling" endpoint +// of the "ServiceResultCollectionUserTypeSibling" service. +func NewResulttypesiblingcollectionResponseCollection(res serviceresultcollectionusertypesiblingviews.ResulttypesiblingcollectionCollectionView) ResulttypesiblingcollectionResponseCollection { + body := make([]*ResulttypesiblingcollectionResponse, len(res)) + for i, val := range res { + if val == nil { + body[i] = nil + continue + } + body[i] = marshalServiceresultcollectionusertypesiblingviewsResulttypesiblingcollectionViewToResulttypesiblingcollectionResponse(val) + } + return body +} diff --git a/http/codegen/testdata/golden/server_types_server-with-result-sibling-user-type-fields.go.golden b/http/codegen/testdata/golden/server_types_server-with-result-sibling-user-type-fields.go.golden new file mode 100644 index 0000000000..fd9a4529a2 --- /dev/null +++ b/http/codegen/testdata/golden/server_types_server-with-result-sibling-user-type-fields.go.golden @@ -0,0 +1,28 @@ +// MethodResultUserTypeSiblingResponseBody is the type of the +// "ServiceResultUserTypeSibling" service "MethodResultUserTypeSibling" +// endpoint HTTP response body. +type MethodResultUserTypeSiblingResponseBody struct { + // Attribute A + A *UserTypeResponseBody `json:"a"` + // Attribute B + B *UserTypeResponseBody `json:"b"` +} + +// UserTypeResponseBody is used to define fields on response body types. +type UserTypeResponseBody struct { + U *int `form:"u,omitempty" json:"u,omitempty" xml:"u,omitempty"` +} + +// NewMethodResultUserTypeSiblingResponseBody builds the HTTP response body +// from the result of the "MethodResultUserTypeSibling" endpoint of the +// "ServiceResultUserTypeSibling" service. +func NewMethodResultUserTypeSiblingResponseBody(res *serviceresultusertypesiblingviews.ResulttypesiblingView) *MethodResultUserTypeSiblingResponseBody { + body := &MethodResultUserTypeSiblingResponseBody{} + if res.A != nil { + body.A = marshalServiceresultusertypesiblingviewsUserTypeViewToUserTypeResponseBody(res.A) + } + if res.B != nil { + body.B = marshalServiceresultusertypesiblingviewsUserTypeViewToUserTypeResponseBody(res.B) + } + return body +} diff --git a/http/codegen/testdata/result_dsls.go b/http/codegen/testdata/result_dsls.go index bb8647d6ed..e9daf6f64a 100644 --- a/http/codegen/testdata/result_dsls.go +++ b/http/codegen/testdata/result_dsls.go @@ -761,6 +761,62 @@ var ResultWithResultCollectionDSL = func() { }) } +// ResultTypeSiblingUserTypeFieldsDSL defines a result type with sibling fields (a, b) +// that both reference the same named type (UserType). This tests the projection cache +// fix for a bug where sibling fields were sharing the same AttributeExpr pointer, +// causing metadata (descriptions and JSON tags) to leak between them. +var ResultTypeSiblingUserTypeFieldsDSL = func() { + var UserType = Type("UserType", func() { + Attribute("u", Int) + }) + + var RT = ResultType("ResultTypeSibling", func() { + Attribute("a", UserType, "Attribute A", func() { + Meta("struct:tag:json", "a") + }) + Attribute("b", UserType, "Attribute B", func() { + Meta("struct:tag:json", "b") + }) + }) + + Service("ServiceResultUserTypeSibling", func() { + Method("MethodResultUserTypeSibling", func() { + Result(RT) + HTTP(func() { + GET("/") + }) + }) + }) +} + +// ResultTypeCollectionSiblingUserTypeFieldsDSL defines a result type collection with +// sibling fields (a, b) that both reference the same named type (UserType). This tests +// the fix for a bug where sibling fields were sharing the same AttributeExpr pointer, +// causing metadata (descriptions and JSON tags) to leak between them. +var ResultTypeCollectionSiblingUserTypeFieldsDSL = func() { + var UserType = Type("UserType", func() { + Attribute("u", Int) + }) + + var RT = ResultType("ResultTypeSiblingCollection", func() { + Attribute("a", UserType, "Attribute A", func() { + Meta("struct:tag:json", "a") + }) + Attribute("b", UserType, "Attribute B", func() { + Meta("struct:tag:json", "b") + }) + }) + + Service("ServiceResultCollectionUserTypeSibling", func() { + Method("MethodResultCollectionUserTypeSibling", func() { + Result(CollectionOf(RT)) + HTTP(func() { + GET("/") + }) + }) + }) +} + var ResultWithCustomPkgTypeDSL = func() { var Foo = Type("Foo", func() { Meta("struct:pkg:path", "foo")