Skip to content

Commit 708b91b

Browse files
authored
impl(rust): generate gcp.resource.name using annotations (#2932)
This commit updates the Rust generator to populate the `gcp.resource.name` tracing attribute in `RequestOptions` by inspecting `google.api.resource_reference` annotations on request fields. Key changes: - Implements a revised heuristic in `annotate.go` that prioritizes annotated fields, including nested fields up to one level deep. - Updates `transport.rs.mustache` to inject runtime logic that evaluates candidate fields in priority order (e.g., `name`, `parent`, `resource`). - Adds `candidateField` struct and `findResourceNameFields` logic to support runtime resolution of the resource name. This ensures that traces generated by the Rust client libraries contain the correct resource name, improving observability and alignment with GCP standards.
1 parent 2218232 commit 708b91b

File tree

7 files changed

+625
-14
lines changed

7 files changed

+625
-14
lines changed

internal/sidekick/api/model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,8 @@ type Field struct {
747747
// - For OpenAPI, it is an optional field
748748
// - For OpenAPI, it has format == "uuid"
749749
AutoPopulated bool
750+
// IsResourceReference is true if the field is annotated with google.api.resource_reference.
751+
IsResourceReference bool
750752
// FieldBehavior indicates how the field behaves in requests and responses.
751753
//
752754
// For example, that a field is required in requests, or given as output

internal/sidekick/parser/protobuf.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -499,14 +499,15 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
499499
for _, mf := range m.Field {
500500
isProtoOptional := mf.Proto3Optional != nil && *mf.Proto3Optional
501501
field := &api.Field{
502-
Name: mf.GetName(),
503-
ID: mFQN + "." + mf.GetName(),
504-
JSONName: mf.GetJsonName(),
505-
Deprecated: mf.GetOptions().GetDeprecated(),
506-
Optional: isProtoOptional,
507-
IsOneOf: mf.OneofIndex != nil && !isProtoOptional,
508-
AutoPopulated: protobufIsAutoPopulated(mf),
509-
Behavior: protobufFieldBehavior(mf),
502+
Name: mf.GetName(),
503+
ID: mFQN + "." + mf.GetName(),
504+
JSONName: mf.GetJsonName(),
505+
Deprecated: mf.GetOptions().GetDeprecated(),
506+
Optional: isProtoOptional,
507+
IsOneOf: mf.OneofIndex != nil && !isProtoOptional,
508+
AutoPopulated: protobufIsAutoPopulated(mf),
509+
IsResourceReference: protobufIsResourceReference(mf),
510+
Behavior: protobufFieldBehavior(mf),
510511
}
511512
normalizeTypes(state, mf, field)
512513
message.Fields = append(message.Fields, field)

internal/sidekick/parser/protobuf_annotations.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,10 @@ func protobufIsAutoPopulated(field *descriptorpb.FieldDescriptorProto) bool {
192192

193193
return true
194194
}
195+
196+
func protobufIsResourceReference(field *descriptorpb.FieldDescriptorProto) bool {
197+
if field.GetType() != descriptorpb.FieldDescriptorProto_TYPE_STRING {
198+
return false
199+
}
200+
return proto.HasExtension(field.GetOptions(), annotations.E_ResourceReference)
201+
}

internal/sidekick/parser/protobuf_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,12 +1595,13 @@ func TestProtobuf_AutoPopulated(t *testing.T) {
15951595
Documentation: "A request to create a `Foo` resource.",
15961596
Fields: []*api.Field{
15971597
{
1598-
Name: "parent",
1599-
JSONName: "parent",
1600-
ID: ".test.CreateFooRequest.parent",
1601-
Documentation: "Required. The resource name of the project.",
1602-
Typez: api.STRING_TYPE,
1603-
Behavior: []api.FieldBehavior{api.FIELD_BEHAVIOR_REQUIRED},
1598+
Name: "parent",
1599+
JSONName: "parent",
1600+
ID: ".test.CreateFooRequest.parent",
1601+
Documentation: "Required. The resource name of the project.",
1602+
Typez: api.STRING_TYPE,
1603+
Behavior: []api.FieldBehavior{api.FIELD_BEHAVIOR_REQUIRED},
1604+
IsResourceReference: true,
16041605
},
16051606
{
16061607
Name: "foo_id",

internal/sidekick/rust/annotate.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ import (
2727
"github.com/iancoleman/strcase"
2828
)
2929

30+
// resourceNameCandidateField represents a potential field to use for the resource name.
31+
type resourceNameCandidateField struct {
32+
FieldPath []string // e.g., ["book"], ["book", "name"]
33+
Field *api.Field
34+
IsNested bool
35+
Accessor string
36+
}
37+
3038
type modelAnnotations struct {
3139
PackageName string
3240
PackageVersion string
@@ -223,6 +231,8 @@ type methodAnnotation struct {
223231
Attributes []string
224232
RoutingRequired bool
225233
DetailedTracingAttributes bool
234+
ResourceNameFields []*resourceNameCandidateField
235+
HasResourceNameFields bool
226236
}
227237

228238
type pathInfoAnnotation struct {
@@ -690,6 +700,139 @@ func (c *codec) addFeatureAnnotations(model *api.API, ann *modelAnnotations) {
690700
}
691701
}
692702

703+
// makeChainAccessor generates the Rust accessor code for a chain of fields.
704+
// It handles optional fields and oneofs correctly.
705+
// parentAccessor is the accessor for the parent message (e.g. "req").
706+
func makeChainAccessor(fields []*api.Field, parentAccessor string) string {
707+
accessor := parentAccessor
708+
for i, field := range fields {
709+
fieldName := toSnake(field.Name)
710+
if i == 0 {
711+
// First field in the chain
712+
if field.IsOneOf {
713+
accessor = fmt.Sprintf("%s.%s()", accessor, fieldName)
714+
} else if field.Optional {
715+
accessor = fmt.Sprintf("%s.%s.as_ref()", accessor, fieldName)
716+
} else {
717+
accessor = fmt.Sprintf("Some(&%s.%s)", accessor, fieldName)
718+
}
719+
} else {
720+
// Subsequent fields (nested)
721+
if field.IsOneOf {
722+
accessor = fmt.Sprintf("%s.and_then(|s| s.%s())", accessor, fieldName)
723+
} else if field.Optional {
724+
accessor = fmt.Sprintf("%s.and_then(|s| s.%s.as_ref())", accessor, fieldName)
725+
} else {
726+
accessor = fmt.Sprintf("%s.map(|s| &s.%s)", accessor, fieldName)
727+
}
728+
}
729+
}
730+
return accessor
731+
}
732+
733+
// findResourceNameCandidates identifies all fields annotated with google.api.resource_reference.
734+
// It searches top-level fields and fields nested one level deep.
735+
func (c *codec) findResourceNameCandidates(m *api.Method) []*resourceNameCandidateField {
736+
var candidates []*resourceNameCandidateField
737+
738+
// Find top-level annotated fields
739+
for _, field := range m.InputType.Fields {
740+
if field.IsResourceReference && !field.Repeated && !field.Map && field.Typez == api.STRING_TYPE {
741+
candidates = append(candidates, &resourceNameCandidateField{
742+
FieldPath: []string{field.Name},
743+
Field: field,
744+
IsNested: false,
745+
Accessor: makeChainAccessor([]*api.Field{field}, "req"),
746+
})
747+
}
748+
}
749+
750+
// Find nested annotated fields (one level deep)
751+
for _, field := range m.InputType.Fields {
752+
if field.MessageType == nil || field.Repeated || field.Map {
753+
continue
754+
}
755+
for _, nestedField := range field.MessageType.Fields {
756+
if !nestedField.IsResourceReference || nestedField.Repeated || nestedField.Map || nestedField.Typez != api.STRING_TYPE {
757+
continue
758+
}
759+
candidates = append(candidates, &resourceNameCandidateField{
760+
FieldPath: []string{field.Name, nestedField.Name},
761+
Field: nestedField,
762+
IsNested: true,
763+
Accessor: makeChainAccessor([]*api.Field{field, nestedField}, "req"),
764+
})
765+
}
766+
}
767+
return candidates
768+
}
769+
770+
func (c *codec) findResourceNameFields(m *api.Method) []*resourceNameCandidateField {
771+
if m.InputType == nil {
772+
return nil
773+
}
774+
775+
candidates := c.findResourceNameCandidates(m)
776+
777+
if len(candidates) == 0 {
778+
return nil
779+
}
780+
781+
// Check for HTTP path presence
782+
var httpParams map[string]bool
783+
if m.PathInfo != nil && m.PathInfo.Codec != nil {
784+
if pia, ok := m.PathInfo.Codec.(*pathInfoAnnotation); ok {
785+
httpParams = make(map[string]bool)
786+
for _, p := range pia.UniqueParameters {
787+
httpParams[p.FieldName] = true
788+
}
789+
}
790+
}
791+
792+
isInPath := func(c *resourceNameCandidateField) bool {
793+
if httpParams == nil {
794+
return false
795+
}
796+
var snakeParts []string
797+
for _, p := range c.FieldPath {
798+
snakeParts = append(snakeParts, toSnake(p))
799+
}
800+
fieldName := strings.Join(snakeParts, ".")
801+
return httpParams[fieldName]
802+
}
803+
804+
slices.SortStableFunc(candidates, compareResourceNameCandidates(isInPath))
805+
806+
return candidates
807+
}
808+
809+
// sortResourceNameCandidates sorts candidates by priority:
810+
// 1. Top-level fields (IsNested == false).
811+
// 2. Fields in HTTP path (isInPath == true).
812+
// 3. Proto definition order (stable sort).
813+
func compareResourceNameCandidates(isInPath func(*resourceNameCandidateField) bool) func(a, b *resourceNameCandidateField) int {
814+
return func(a, b *resourceNameCandidateField) int {
815+
// 1. Top-level before Nested.
816+
if a.IsNested != b.IsNested {
817+
if !a.IsNested {
818+
return -1 // a is top (false), b is nested (true) -> a < b
819+
}
820+
return 1
821+
}
822+
// 2. In-Path before Not-In-Path.
823+
inPathA := isInPath(a)
824+
inPathB := isInPath(b)
825+
if inPathA != inPathB {
826+
if inPathA {
827+
return -1 // a is in-path (true), b is not (false) -> a < b
828+
}
829+
return 1
830+
}
831+
// 3. Stable sort preserves proto order.
832+
return 0
833+
}
834+
}
835+
693836
// packageToModuleName maps "google.foo.v1" to "google::foo::v1".
694837
func packageToModuleName(p string) string {
695838
components := strings.Split(p, ".")
@@ -805,6 +948,7 @@ func (c *codec) annotateMethod(m *api.Method) {
805948
returnType = "()"
806949
}
807950
serviceName := c.ServiceName(m.Service)
951+
resourceNameFields := c.findResourceNameFields(m)
808952
annotation := &methodAnnotation{
809953
Name: toSnake(m.Name),
810954
NameNoMangling: toSnakeNoMangling(m.Name),
@@ -820,6 +964,8 @@ func (c *codec) annotateMethod(m *api.Method) {
820964
HasVeneer: c.hasVeneer,
821965
RoutingRequired: c.routingRequired,
822966
DetailedTracingAttributes: c.detailedTracingAttributes,
967+
ResourceNameFields: resourceNameFields,
968+
HasResourceNameFields: len(resourceNameFields) > 0,
823969
}
824970
if annotation.Name == "clone" {
825971
// Some methods look too similar to standard Rust traits. Clippy makes

0 commit comments

Comments
 (0)