From 9badc7323042329033bd2a6f6d723a3e33ecedb8 Mon Sep 17 00:00:00 2001 From: Adam Bocim Date: Wed, 17 Jun 2026 07:02:22 +0200 Subject: [PATCH] fix: http server codegen Sibling fields in result types were sharing the same AttributeExpr pointer during projection, causing metadata (descriptions and JSON tags) to leak between them. Added field name to the `seen` key in `projectRecursive` to be ensured each sibling gets its own isolated entry. Signed-off-by: Adam Bocim --- expr/result_type.go | 12 +++- http/codegen/server_types_test.go | 2 + ...lection-sibling-user-type-fields.go.golden | 33 +++++++++++ ...-result-sibling-user-type-fields.go.golden | 28 ++++++++++ http/codegen/testdata/result_dsls.go | 56 +++++++++++++++++++ 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 http/codegen/testdata/golden/server_types_server-with-result-collection-sibling-user-type-fields.go.golden create mode 100644 http/codegen/testdata/golden/server_types_server-with-result-sibling-user-type-fields.go.golden 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")