Skip to content

Commit 69baaf4

Browse files
abocimraphael
andauthored
fix: http server codegen (#3944)
* 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 <adam.bocim@seznam.cz> * Memoize projected types rather than field attributes in view projection The projection cache stored entire field attributes, so any two fields hashing to the same (type, view) key aliased one AttributeExpr and leaked per-field metadata (descriptions, struct tags) across fields. Keying by field name only narrowed the collisions: same-named fields of the same type under different parent types still aliased. Cache the projected types instead and give every field its own attribute wrapping the shared projection. Single result types register their projection before computing their fields so that recursive references resolve to the in-flight projection and terminate. --------- Signed-off-by: Adam Bocim <adam.bocim@seznam.cz> Co-authored-by: Raphael Simon <simon.raphael@gmail.com>
1 parent 042efad commit 69baaf4

7 files changed

Lines changed: 306 additions & 36 deletions

expr/project_test.go

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"fmt"
55
"strings"
66
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
710
)
811

912
var (
@@ -27,8 +30,9 @@ var (
2730
compositeResultDefault = resultType("a", object(collectionResultDefault), "b", String)
2831
compositeResultLink = resultType("a", object(collectionResultLink))
2932

30-
recursiveResult = resultRecursive("a", String, view("default", "a", object(String)))
31-
embeddedRecursiveResult = resultType("a", String, "rec", recursiveResult)
33+
// recursiveResult is its own expected projection: projecting a recursive
34+
// result type yields a single projected type that references itself.
35+
recursiveResult = resultRecursive("a", String, view("default", "a", object(String)))
3236
)
3337

3438
func init() {
@@ -50,7 +54,7 @@ func TestProject(t *testing.T) {
5054
{"collection-link", collectionResult, "link", collectionResultLink},
5155
{"composite-default", compositeResult, "default", compositeResultDefault},
5256
{"composite-link", compositeResult, "link", compositeResultLink},
53-
{"recursive", recursiveResult, "default", embeddedRecursiveResult},
57+
{"recursive", recursiveResult, "default", recursiveResult},
5458
}
5559
for _, k := range cases {
5660
t.Run(k.Name, func(t *testing.T) {
@@ -78,6 +82,73 @@ func TestProject(t *testing.T) {
7882
}
7983
}
8084

85+
// TestProjectDoesNotAliasFieldAttributes verifies that fields sharing a type
86+
// share the projected type but never the AttributeExpr wrapping it, so that
87+
// per-field metadata such as descriptions does not leak across fields.
88+
func TestProjectDoesNotAliasFieldAttributes(t *testing.T) {
89+
t.Run("sibling user type fields", func(t *testing.T) {
90+
shared := userType("Shared", object(Int))
91+
rt := resultType("a", shared, "b", shared,
92+
view("default", "a", object(Int), "b", object(Int)))
93+
94+
projected, err := Project(rt, "default")
95+
require.NoError(t, err)
96+
97+
obj := AsObject(projected.Type)
98+
a, b := obj.Attribute("a"), obj.Attribute("b")
99+
assert.NotSame(t, a, b)
100+
assert.Equal(t, "desc a", a.Description)
101+
assert.Equal(t, "desc b", b.Description)
102+
assert.Same(t, a.Type, b.Type)
103+
})
104+
105+
t.Run("sibling result type fields", func(t *testing.T) {
106+
rt := resultType("x", simpleResult, "y", simpleResult,
107+
view("default", "x", AsObject(simpleResult), "y", AsObject(simpleResult)))
108+
109+
projected, err := Project(rt, "default")
110+
require.NoError(t, err)
111+
112+
obj := AsObject(projected.Type)
113+
x, y := obj.Attribute("x"), obj.Attribute("y")
114+
assert.NotSame(t, x, y)
115+
assert.Equal(t, "desc x", x.Description)
116+
assert.Equal(t, "desc y", y.Description)
117+
assert.Same(t, x.Type, y.Type)
118+
})
119+
120+
t.Run("same field in different parent types", func(t *testing.T) {
121+
shared := userType("Shared", object(Int))
122+
wrapper := userType("Wrapper", &Object{
123+
{Name: "a", Attribute: &AttributeExpr{Type: shared, Description: "Inner A"}},
124+
})
125+
rt := resultType("a", shared, "nested", wrapper,
126+
view("default",
127+
"a", object(Int),
128+
"nested", &Object{{Name: "a", Attribute: &AttributeExpr{Type: object(Int)}}}))
129+
130+
projected, err := Project(rt, "default")
131+
require.NoError(t, err)
132+
133+
obj := AsObject(projected.Type)
134+
outer := obj.Attribute("a")
135+
assert.Equal(t, "desc a", outer.Description)
136+
inner := AsObject(obj.Attribute("nested").Type).Attribute("a")
137+
assert.NotSame(t, outer, inner)
138+
assert.Equal(t, "Inner A", inner.Description)
139+
assert.Same(t, outer.Type, inner.Type)
140+
})
141+
142+
t.Run("recursive result type references its own projection", func(t *testing.T) {
143+
projected, err := Project(recursiveResult, "default")
144+
require.NoError(t, err)
145+
146+
rec := AsObject(projected.Type).Attribute("rec")
147+
assert.Equal(t, "desc rec", rec.Description)
148+
assert.Same(t, projected, rec.Type)
149+
})
150+
}
151+
81152
// view is a helper function for building view expressions used in tests. name
82153
// is the name of the view, attributes list the names of the attributes rendered
83154
// by the view. name may use the format "name:view" in which case view is the

expr/result_type.go

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -232,22 +232,32 @@ func (rt *ResultTypeExpr) ensureDefaultView() {
232232
// individual attributes may use a different view. In this case Project uses
233233
// that view and returns an error if it isn't defined on the attribute type.
234234
func Project(rt *ResultTypeExpr, view string) (*ResultTypeExpr, error) {
235-
return project(rt, view, make(map[string]*AttributeExpr))
235+
return project(rt, view, make(map[string]UserType))
236236
}
237237

238-
func project(rt *ResultTypeExpr, view string, seen map[string]*AttributeExpr) (*ResultTypeExpr, error) {
238+
// project computes the projection of rt for view. seen memoizes projected
239+
// types keyed by (type hash, view). It caches types only - never field
240+
// attributes - so that sibling fields referencing the same type each keep
241+
// their own AttributeExpr (description and meta would otherwise leak across
242+
// fields). projectSingle registers its projection before computing the fields
243+
// so that recursive references resolve to the in-flight projection and the
244+
// recursion terminates.
245+
func project(rt *ResultTypeExpr, view string, seen map[string]UserType) (*ResultTypeExpr, error) {
239246
_, params, _ := mime.ParseMediaType(rt.Identifier)
240247
if params["view"] == view {
241248
// nothing to do
242249
return rt, nil
243250
}
251+
if p, ok := seen[hashTypeAndView(rt, view)]; ok {
252+
return p.(*ResultTypeExpr), nil
253+
}
244254
if _, ok := rt.Type.(*Array); ok {
245255
return projectCollection(rt, view, seen)
246256
}
247257
return projectSingle(rt, view, seen)
248258
}
249259

250-
func projectSingle(rt *ResultTypeExpr, view string, seen map[string]*AttributeExpr) (*ResultTypeExpr, error) {
260+
func projectSingle(rt *ResultTypeExpr, view string, seen map[string]UserType) (*ResultTypeExpr, error) {
251261
v := rt.View(view)
252262
if v == nil {
253263
return nil, fmt.Errorf("unknown view %#v", view)
@@ -280,26 +290,15 @@ func projectSingle(rt *ResultTypeExpr, view string, seen map[string]*AttributeEx
280290
typeName += Title(view)
281291
}
282292

283-
var ut *UserTypeExpr
284-
if att, ok := seen[hashAttrAndView(rt.Attribute(), view)]; ok {
285-
if rt, ok2 := att.Type.(*ResultTypeExpr); ok2 {
286-
ut = &UserTypeExpr{
287-
AttributeExpr: DupAtt(rt.Attribute()),
288-
TypeName: rt.TypeName,
289-
}
290-
}
291-
}
292293
id := rt.projectIdentifier(view)
293-
if ut == nil {
294-
ut = &UserTypeExpr{
295-
AttributeExpr: &AttributeExpr{
296-
Description: desc,
297-
Validation: val,
298-
},
299-
}
294+
ut := &UserTypeExpr{
295+
AttributeExpr: &AttributeExpr{
296+
Description: desc,
297+
Validation: val,
298+
},
299+
TypeName: typeName,
300+
UID: id,
300301
}
301-
ut.TypeName = typeName
302-
ut.UID = id
303302
ut.Type = Dup(v.Type)
304303
ut.UserExamples = v.UserExamples
305304
projected := &ResultTypeExpr{
@@ -312,6 +311,11 @@ func projectSingle(rt *ResultTypeExpr, view string, seen map[string]*AttributeEx
312311
Parent: projected,
313312
}}
314313

314+
// Register the projection before computing the fields so that recursive
315+
// references to rt resolve to the in-flight projection - its fields are
316+
// filled in place below - and the recursion terminates.
317+
seen[hashTypeAndView(rt, view)] = projected
318+
315319
projectedObj := projected.Type.(*Object)
316320
mtObj := AsObject(rt.Type)
317321
for _, nat := range *viewObj {
@@ -326,7 +330,7 @@ func projectSingle(rt *ResultTypeExpr, view string, seen map[string]*AttributeEx
326330
return projected, nil
327331
}
328332

329-
func projectCollection(rt *ResultTypeExpr, view string, seen map[string]*AttributeExpr) (*ResultTypeExpr, error) {
333+
func projectCollection(rt *ResultTypeExpr, view string, seen map[string]UserType) (*ResultTypeExpr, error) {
330334
// Project the collection element result type
331335
e := rt.Type.(*Array).ElemType.Type.(*ResultTypeExpr) // validation checked this cast would work
332336
pe, err2 := project(e, view, seen)
@@ -359,13 +363,16 @@ func projectCollection(rt *ResultTypeExpr, view string, seen map[string]*Attribu
359363
return nil, eval.Context.Errors
360364
}
361365

366+
seen[hashTypeAndView(rt, view)] = proj
362367
return proj, nil
363368
}
364369

365-
func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, seen map[string]*AttributeExpr) (*AttributeExpr, error) {
366-
if att, ok := seen[hashAttrAndView(at, view)]; ok {
367-
return att, nil
368-
}
370+
// projectRecursive computes the projected attribute for the field described
371+
// by at within a result type being projected with view. vat is the matching
372+
// view attribute. It always returns a fresh attribute: projected types are
373+
// shared through seen but the attributes wrapping them never are, so that
374+
// per-field metadata does not leak across fields of the same type.
375+
func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, seen map[string]UserType) (*AttributeExpr, error) {
369376
at = DupAtt(at)
370377

371378
if rt, ok := at.Type.(*ResultTypeExpr); ok {
@@ -378,7 +385,6 @@ func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, s
378385
view = DefaultView
379386
}
380387
}
381-
seen[hashAttrAndView(at, view)] = at
382388
pr, err := project(rt, view, seen)
383389
if err != nil {
384390
return nil, fmt.Errorf("view %#v on field %#v cannot be computed: %w", view, vat.Name, err)
@@ -387,8 +393,15 @@ func projectRecursive(at *AttributeExpr, vat *NamedAttributeExpr, view string, s
387393
return at, nil
388394
}
389395

390-
if _, ok := at.Type.(*UserTypeExpr); ok {
391-
seen[hashAttrAndView(at, view)] = at
396+
if ut, ok := at.Type.(*UserTypeExpr); ok {
397+
key := hashTypeAndView(ut, view)
398+
if p, ok := seen[key]; ok {
399+
at.Type = p
400+
return at, nil
401+
}
402+
// Register before recursing into the fields below (they are projected
403+
// in place) so that recursive user types terminate.
404+
seen[key] = ut
392405
}
393406

394407
if obj := AsObject(at.Type); obj != nil {
@@ -457,8 +470,8 @@ func (v *ViewExpr) EvalName() string {
457470
return prefix + suffix
458471
}
459472

460-
// hashAttrAndView computes a hash for an attribute and a view that returns the
461-
// same value for two attributes and views that produce the same projected type.
462-
func hashAttrAndView(att *AttributeExpr, view string) string {
463-
return Hash(att.Type, false, false, false) + "::" + view
473+
// hashTypeAndView computes the projection cache key for the given type and
474+
// view. Two types with the same key project to the same type for the view.
475+
func hashTypeAndView(t DataType, view string) string {
476+
return Hash(t, false, false, false) + "::" + view
464477
}

http/codegen/server_types_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func TestServerTypes(t *testing.T) {
2525
{"server-result-type-validate", testdata.ResultTypeValidateDSL},
2626
{"server-with-result-collection", testdata.ResultWithResultCollectionDSL},
2727
{"server-with-result-view", testdata.ResultWithResultViewDSL},
28+
{"server-with-result-sibling-user-type-fields", testdata.ResultTypeSiblingUserTypeFieldsDSL},
29+
{"server-with-result-collection-sibling-user-type-fields", testdata.ResultTypeCollectionSiblingUserTypeFieldsDSL},
30+
{"server-with-result-nested-user-type-fields", testdata.ResultTypeNestedUserTypeFieldsDSL},
2831
{"server-empty-error-response-body", testdata.EmptyErrorResponseBodyDSL},
2932
{"server-with-error-custom-pkg", testdata.WithErrorCustomPkgDSL},
3033
{"server-body-custom-name", testdata.PayloadBodyCustomNameDSL},
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// ResulttypesiblingcollectionResponseCollection is the type of the
2+
// "ServiceResultCollectionUserTypeSibling" service
3+
// "MethodResultCollectionUserTypeSibling" endpoint HTTP response body.
4+
type ResulttypesiblingcollectionResponseCollection []*ResulttypesiblingcollectionResponse
5+
6+
// ResulttypesiblingcollectionResponse is used to define fields on response
7+
// body types.
8+
type ResulttypesiblingcollectionResponse struct {
9+
// Attribute A
10+
A *UserTypeResponse `json:"a"`
11+
// Attribute B
12+
B *UserTypeResponse `json:"b"`
13+
}
14+
15+
// UserTypeResponse is used to define fields on response body types.
16+
type UserTypeResponse struct {
17+
U *int `form:"u,omitempty" json:"u,omitempty" xml:"u,omitempty"`
18+
}
19+
20+
// NewResulttypesiblingcollectionResponseCollection builds the HTTP response
21+
// body from the result of the "MethodResultCollectionUserTypeSibling" endpoint
22+
// of the "ServiceResultCollectionUserTypeSibling" service.
23+
func NewResulttypesiblingcollectionResponseCollection(res serviceresultcollectionusertypesiblingviews.ResulttypesiblingcollectionCollectionView) ResulttypesiblingcollectionResponseCollection {
24+
body := make([]*ResulttypesiblingcollectionResponse, len(res))
25+
for i, val := range res {
26+
if val == nil {
27+
body[i] = nil
28+
continue
29+
}
30+
body[i] = marshalServiceresultcollectionusertypesiblingviewsResulttypesiblingcollectionViewToResulttypesiblingcollectionResponse(val)
31+
}
32+
return body
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// MethodResultUserTypeNestedResponseBody is the type of the
2+
// "ServiceResultUserTypeNested" service "MethodResultUserTypeNested" endpoint
3+
// HTTP response body.
4+
type MethodResultUserTypeNestedResponseBody struct {
5+
// Outer A
6+
A *UserTypeResponseBody `json:"outer_a"`
7+
Nested *WrapperResponseBody `form:"nested,omitempty" json:"nested,omitempty" xml:"nested,omitempty"`
8+
}
9+
10+
// UserTypeResponseBody is used to define fields on response body types.
11+
type UserTypeResponseBody struct {
12+
U *int `form:"u,omitempty" json:"u,omitempty" xml:"u,omitempty"`
13+
}
14+
15+
// WrapperResponseBody is used to define fields on response body types.
16+
type WrapperResponseBody struct {
17+
// Inner A
18+
A *UserTypeResponseBody `json:"inner_a"`
19+
}
20+
21+
// NewMethodResultUserTypeNestedResponseBody builds the HTTP response body from
22+
// the result of the "MethodResultUserTypeNested" endpoint of the
23+
// "ServiceResultUserTypeNested" service.
24+
func NewMethodResultUserTypeNestedResponseBody(res *serviceresultusertypenestedviews.ResulttypenestedView) *MethodResultUserTypeNestedResponseBody {
25+
body := &MethodResultUserTypeNestedResponseBody{}
26+
if res.A != nil {
27+
body.A = marshalServiceresultusertypenestedviewsUserTypeViewToUserTypeResponseBody(res.A)
28+
}
29+
if res.Nested != nil {
30+
body.Nested = marshalServiceresultusertypenestedviewsWrapperViewToWrapperResponseBody(res.Nested)
31+
}
32+
return body
33+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// MethodResultUserTypeSiblingResponseBody is the type of the
2+
// "ServiceResultUserTypeSibling" service "MethodResultUserTypeSibling"
3+
// endpoint HTTP response body.
4+
type MethodResultUserTypeSiblingResponseBody struct {
5+
// Attribute A
6+
A *UserTypeResponseBody `json:"a"`
7+
// Attribute B
8+
B *UserTypeResponseBody `json:"b"`
9+
}
10+
11+
// UserTypeResponseBody is used to define fields on response body types.
12+
type UserTypeResponseBody struct {
13+
U *int `form:"u,omitempty" json:"u,omitempty" xml:"u,omitempty"`
14+
}
15+
16+
// NewMethodResultUserTypeSiblingResponseBody builds the HTTP response body
17+
// from the result of the "MethodResultUserTypeSibling" endpoint of the
18+
// "ServiceResultUserTypeSibling" service.
19+
func NewMethodResultUserTypeSiblingResponseBody(res *serviceresultusertypesiblingviews.ResulttypesiblingView) *MethodResultUserTypeSiblingResponseBody {
20+
body := &MethodResultUserTypeSiblingResponseBody{}
21+
if res.A != nil {
22+
body.A = marshalServiceresultusertypesiblingviewsUserTypeViewToUserTypeResponseBody(res.A)
23+
}
24+
if res.B != nil {
25+
body.B = marshalServiceresultusertypesiblingviewsUserTypeViewToUserTypeResponseBody(res.B)
26+
}
27+
return body
28+
}

0 commit comments

Comments
 (0)