@@ -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+
3038type 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
228238type 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".
694837func 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