Skip to content

Commit d903b0d

Browse files
authored
fix(gengapic): Ensure stable order of dynamic Firestore routing header parameters (#1685)
1 parent 34860b5 commit d903b0d

File tree

3 files changed

+133
-3
lines changed

3 files changed

+133
-3
lines changed

internal/gengapic/generator.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ var enableWrapperTypesForPageSize = map[string]bool{
4141
"google.cloud.bigquery.v2": true,
4242
}
4343

44+
var enableOrderedRoutingHeaders = map[string]bool{
45+
"google.firestore.v1": true,
46+
"google.firestore.admin.v1": true,
47+
}
48+
4449
var enableMtlsHardBoundTokens = map[string]bool{
4550
"bigquery.googleapis.com": true,
4651
"cloudasset.googleapis.com": true,

internal/gengapic/gengapic.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,26 @@ func (g *generator) insertDynamicRequestHeaders(m *descriptorpb.MethodDescriptor
457457
g.printf(" routingHeadersMap[%q] = %s", headerName, regexHelper)
458458
g.printf("}")
459459
}
460-
g.printf("for headerName, headerValue := range routingHeadersMap {")
461-
g.printf(` routingHeaders = fmt.Sprintf("%%s%%s=%%s&", routingHeaders, headerName, headerValue)`)
462-
g.printf("}")
460+
var orderedHeaders bool
461+
for p, ok := range enableOrderedRoutingHeaders {
462+
if g.descInfo.ParentFile[m].GetPackage() == p && ok {
463+
orderedHeaders = true
464+
break
465+
}
466+
}
467+
if orderedHeaders {
468+
for i := range headers {
469+
headerName := headers[i][2]
470+
g.printf("if headerValue, ok := routingHeadersMap[%q]; ok {", headerName)
471+
g.printf(` routingHeaders = fmt.Sprintf("%%s%%s=%%s&", routingHeaders, %q, headerValue)`, headerName)
472+
g.printf(" delete(routingHeadersMap, %q)", headerName)
473+
g.printf("}")
474+
}
475+
} else {
476+
g.printf("for headerName, headerValue := range routingHeadersMap {")
477+
g.printf(` routingHeaders = fmt.Sprintf("%%s%%s=%%s&", routingHeaders, headerName, headerValue)`)
478+
g.printf("}")
479+
}
463480
g.printf(`routingHeaders = strings.TrimSuffix(routingHeaders, "&")`)
464481
g.imports[pbinfo.ImportSpec{Path: "strings"}] = true
465482
g.printf(`hds := []string{"x-goog-request-params", routingHeaders}`)

internal/gengapic/gengapic_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,114 @@ func TestGenAndCommentHelpers(t *testing.T) {
14141414
}
14151415
}
14161416

1417+
func TestInsertDynamicRequestHeaders_Ordering(t *testing.T) {
1418+
g := &generator{
1419+
imports: make(map[pbinfo.ImportSpec]bool),
1420+
descInfo: pbinfo.Info{
1421+
Type: make(map[string]pbinfo.ProtoType),
1422+
ParentFile: make(map[protoreflect.ProtoMessage]*descriptorpb.FileDescriptorProto),
1423+
},
1424+
}
1425+
1426+
m := &descriptorpb.MethodDescriptorProto{
1427+
Name: proto.String("TestMethod"),
1428+
InputType: proto.String(".InputType"),
1429+
Options: &descriptorpb.MethodOptions{},
1430+
}
1431+
1432+
// Setup InputType with fields used in headers
1433+
inputType := &descriptorpb.DescriptorProto{
1434+
Name: proto.String("InputType"),
1435+
Field: []*descriptorpb.FieldDescriptorProto{
1436+
{Name: proto.String("field1"), Type: typePtr(descriptorpb.FieldDescriptorProto_TYPE_STRING)},
1437+
{Name: proto.String("field2"), Type: typePtr(descriptorpb.FieldDescriptorProto_TYPE_STRING)},
1438+
{Name: proto.String("field3"), Type: typePtr(descriptorpb.FieldDescriptorProto_TYPE_STRING)},
1439+
},
1440+
}
1441+
g.descInfo.Type[".InputType"] = inputType
1442+
1443+
// The input headers list defines the order:
1444+
// 1. header_a (from field1)
1445+
// 2. header_b (from field2)
1446+
// 3. header_a (from field3) -- Duplicate key
1447+
headers := [][]string{
1448+
{"regex1", "field1", "header_a"},
1449+
{"regex2", "field2", "header_b"},
1450+
{"regex3", "field3", "header_a"},
1451+
}
1452+
1453+
tests := []struct {
1454+
pkgName string
1455+
wantOrdered bool
1456+
}{
1457+
{"google.firestore.v1", true},
1458+
{"google.firestore.admin.v1", true},
1459+
{"google.cloud.storage.v1", false},
1460+
{"other.package", false},
1461+
}
1462+
1463+
for _, tc := range tests {
1464+
t.Run(tc.pkgName, func(t *testing.T) {
1465+
g.reset()
1466+
1467+
file := &descriptorpb.FileDescriptorProto{
1468+
Package: proto.String(tc.pkgName),
1469+
}
1470+
g.descInfo.ParentFile[m] = file
1471+
g.descInfo.ParentFile[inputType] = file
1472+
1473+
err := g.insertDynamicRequestHeaders(m, headers)
1474+
if err != nil {
1475+
t.Fatalf("insertDynamicRequestHeaders failed: %v", err)
1476+
}
1477+
1478+
got := g.pt.String()
1479+
1480+
if tc.wantOrdered {
1481+
// Verify strict order of checks in the generated code.
1482+
// We expect code blocks for header_a, then header_b, then header_a again.
1483+
1484+
idxA1 := strings.Index(got, `if headerValue, ok := routingHeadersMap["header_a"]; ok {`)
1485+
if idxA1 == -1 {
1486+
t.Errorf("expected first check for header_a")
1487+
}
1488+
1489+
// Look for header_b check AFTER header_a check
1490+
idxB := strings.Index(got[idxA1+1:], `if headerValue, ok := routingHeadersMap["header_b"]; ok {`)
1491+
if idxB == -1 {
1492+
t.Errorf("expected check for header_b after first header_a")
1493+
}
1494+
idxB += idxA1 + 1 // Adjust index to be absolute
1495+
1496+
// Look for second header_a check AFTER header_b check
1497+
idxA2 := strings.Index(got[idxB+1:], `if headerValue, ok := routingHeadersMap["header_a"]; ok {`)
1498+
if idxA2 == -1 {
1499+
t.Errorf("expected second check for header_a after header_b")
1500+
}
1501+
1502+
// Verify delete is called
1503+
if !strings.Contains(got, `delete(routingHeadersMap, "header_a")`) {
1504+
t.Errorf("expected delete map key for %s", tc.pkgName)
1505+
}
1506+
1507+
// Verify absence of range loop
1508+
if strings.Contains(got, `range routingHeadersMap`) {
1509+
t.Errorf("expected NO range map iteration for %s", tc.pkgName)
1510+
}
1511+
} else {
1512+
// Check for unordered iteration pattern: range loop
1513+
if !strings.Contains(got, `for headerName, headerValue := range routingHeadersMap {`) {
1514+
t.Errorf("expected range map iteration for %s, got:\n%s", tc.pkgName, got)
1515+
}
1516+
}
1517+
})
1518+
}
1519+
}
1520+
1521+
func typePtr(t descriptorpb.FieldDescriptorProto_Type) *descriptorpb.FieldDescriptorProto_Type {
1522+
return &t
1523+
}
1524+
14171525
func setHTTPOption(o *descriptorpb.MethodOptions, pattern string) {
14181526
proto.SetExtension(o, annotations.E_Http, &annotations.HttpRule{
14191527
Pattern: &annotations.HttpRule_Get{

0 commit comments

Comments
 (0)