@@ -40,6 +40,34 @@ type ReferenceField struct {
4040 ReferencedGroup string
4141 ReferencedVersion string
4242 RequiredSegments []bool
43+ ArrayBoundaries []ArrayBoundary // all array boundaries (empty if not array-based)
44+ }
45+
46+ // ArrayBoundary represents a single array level in a nested array path
47+ type ArrayBoundary struct {
48+ ArrayPath string // path to the array container
49+ ItemPath string // path within array item to the next array or final field
50+ }
51+
52+ // IsArrayBased returns true if the reference is inside an array
53+ func (r ReferenceField ) IsArrayBased () bool {
54+ return len (r .ArrayBoundaries ) > 0
55+ }
56+
57+ // ArrayPath returns the path to the first array container (for backwards compatibility)
58+ func (r ReferenceField ) ArrayPath () string {
59+ if len (r .ArrayBoundaries ) == 0 {
60+ return ""
61+ }
62+ return r .ArrayBoundaries [0 ].ArrayPath
63+ }
64+
65+ // ItemPath returns the path within the last array item (for backwards compatibility)
66+ func (r ReferenceField ) ItemPath () string {
67+ if len (r .ArrayBoundaries ) == 0 {
68+ return ""
69+ }
70+ return r .ArrayBoundaries [len (r .ArrayBoundaries )- 1 ].ItemPath
4371}
4472
4573type IndexerInfo struct {
@@ -223,13 +251,17 @@ func processKubernetesMapping(v map[string]any, path string, requiredSegments []
223251 reqCopy := make ([]bool , len (requiredSegments ))
224252 copy (reqCopy , requiredSegments )
225253
254+ // Parse all array boundaries for array support (single-level and nested)
255+ arrayBoundaries := parseAllArrayBoundaries (path )
256+
226257 ref := ReferenceField {
227258 FieldName : fieldName ,
228259 FieldPath : path ,
229260 ReferencedKind : kind ,
230261 ReferencedGroup : group ,
231262 ReferencedVersion : version ,
232263 RequiredSegments : reqCopy ,
264+ ArrayBoundaries : arrayBoundaries ,
233265 }
234266 * references = append (* references , ref )
235267 }
@@ -328,14 +360,9 @@ func GenerateIndexers(resultPath, crdKind, indexerOutDir string) error {
328360 }
329361
330362 // Group references by target kind (e.g., all Secret refs together, all Group refs together)
331- // Skip array-based references for now as they require iteration logic
363+ // Both single-level and nested arrays are now supported
332364 refsByKind := make (map [string ][]ReferenceField )
333365 for _ , ref := range references {
334- // Skip references that are arrays for now
335- if strings .Contains (ref .FieldPath , ".items." ) {
336- fmt .Printf ("Skipping array-based reference %s in %s (array indexing not yet supported)\n " , ref .FieldName , crdKind )
337- continue
338- }
339366 refsByKind [ref .ReferencedKind ] = append (refsByKind [ref .ReferencedKind ], ref )
340367 }
341368
@@ -464,36 +491,132 @@ func generateFieldExtractionCode(fields []ReferenceField) []jen.Code {
464491 code := make ([]jen.Code , 0 )
465492
466493 for _ , field := range fields {
467- // Build the field path from the FieldPath
468- // FieldPath looks like: "properties.spec.properties.<version>.properties.groupRef"
469- // We need to convert this to: resource.Spec.<version>.GroupRef
470- fieldAccessPath := buildFieldAccessPath (field .FieldPath )
494+ // Check if this is an array-based reference
495+ if field .IsArrayBased () {
496+ code = append (code , generateArrayFieldExtractionCode (field ))
497+ } else {
498+ // Original non-array logic
499+ fieldAccessPath := buildFieldAccessPath (field .FieldPath )
500+
501+ // Nil check if conditions
502+ nilCheckCondition := buildNilCheckConditions (fieldAccessPath , field .RequiredSegments )
503+
504+ // Add check that the Name field is not empty
505+ condition := nilCheckCondition .Op ("&&" ).Add (
506+ jen .Id (fieldAccessPath ).Dot ("Name" ).Op ("!=" ).Lit ("" ),
507+ )
508+
509+ // Generate: if <nil checks> && resource.Spec.<version>.GroupRef.Name != "" {
510+ // keys = append(keys, types.NamespacedName{...}.String())
511+ // }
512+ code = append (code ,
513+ jen .If (condition ).Block (
514+ jen .Id ("keys" ).Op ("=" ).Append (
515+ jen .Id ("keys" ),
516+ jen .Qual ("k8s.io/apimachinery/pkg/types" , "NamespacedName" ).Values (jen.Dict {
517+ jen .Id ("Name" ): jen .Id (fieldAccessPath ).Dot ("Name" ),
518+ jen .Id ("Namespace" ): jen .Id ("resource" ).Dot ("Namespace" ),
519+ }).Dot ("String" ).Call (),
520+ ),
521+ ),
522+ )
523+ }
524+ }
471525
472- // Nil check if conditions
473- nilCheckCondition := buildNilCheckConditions ( fieldAccessPath , field . RequiredSegments )
526+ return code
527+ }
474528
475- // Add check that the Name field is not empty
476- condition := nilCheckCondition .Op ("&&" ).Add (
477- jen .Id (fieldAccessPath ).Dot ("Name" ).Op ("!=" ).Lit ("" ),
478- )
529+ // generateArrayFieldExtractionCode generates code for extracting keys from array-based references.
530+ // This handles both single-level and nested arrays by generating appropriate nested loops.
531+ // Example for nested path: properties.spec.properties.regions.items.properties.notifications.items.properties.secretRef
532+ // Generates:
533+ //
534+ // if resource.Spec.Regions != nil {
535+ // for _, region := range *resource.Spec.Regions {
536+ // if region.Notifications != nil {
537+ // for _, notification := range *region.Notifications {
538+ // if notification.SecretRef != nil && notification.SecretRef.Name != "" {
539+ // keys = append(keys, ...)
540+ // }
541+ // }
542+ // }
543+ // }
544+ // }
545+ func generateArrayFieldExtractionCode (field ReferenceField ) jen.Code {
546+ boundaries := field .ArrayBoundaries
547+
548+ // Build the nested structure from innermost to outermost
549+ // Start with the innermost block (the append statement)
550+ lastBoundary := boundaries [len (boundaries )- 1 ]
551+ lastItemPath := buildFieldAccessPath (lastBoundary .ItemPath )
552+
553+ // Determine the last loop variable name (from this boundary's array)
554+ lastArrayParts := strings .Split (lastBoundary .ArrayPath , "." )
555+ lastArrayFieldName := lastArrayParts [len (lastArrayParts )- 1 ]
556+ lastLoopVar := generateLoopVariableName (lastArrayFieldName )
557+
558+ // Build the final field path using the last loop variable
559+ finalFieldPath := strings .Replace (lastItemPath , "resource" , lastLoopVar , 1 )
560+
561+ // Create the innermost block: if check and keys append
562+ innermostBlock := jen .If (
563+ jen .Id (finalFieldPath ).Op ("!=" ).Nil ().Op ("&&" ).Add (
564+ jen .Id (finalFieldPath ).Dot ("Name" ).Op ("!=" ).Lit ("" ),
565+ ),
566+ ).Block (
567+ jen .Id ("keys" ).Op ("=" ).Append (
568+ jen .Id ("keys" ),
569+ jen .Qual ("k8s.io/apimachinery/pkg/types" , "NamespacedName" ).Values (jen.Dict {
570+ jen .Id ("Name" ): jen .Id (finalFieldPath ).Dot ("Name" ),
571+ jen .Id ("Namespace" ): jen .Id ("resource" ).Dot ("Namespace" ),
572+ }).Dot ("String" ).Call (),
573+ ),
574+ )
479575
480- // Generate: if <nil checks> && resource.Spec.<version>.GroupRef.Name != "" {
481- // keys = append(keys, types.NamespacedName{...}.String())
482- // }
483- code = append (code ,
484- jen .If (condition ).Block (
485- jen .Id ("keys" ).Op ("=" ).Append (
486- jen .Id ("keys" ),
487- jen .Qual ("k8s.io/apimachinery/pkg/types" , "NamespacedName" ).Values (jen.Dict {
488- jen .Id ("Name" ): jen .Id (fieldAccessPath ).Dot ("Name" ),
489- jen .Id ("Namespace" ): jen .Id ("resource" ).Dot ("Namespace" ),
490- }).Dot ("String" ).Call (),
491- ),
492- ),
576+ // Now build the nested loops from innermost to outermost
577+ currentBlock := innermostBlock
578+
579+ // Process boundaries from the last down to index 1 (skip the first boundary for now)
580+ for i := len (boundaries ) - 1 ; i >= 1 ; i -- {
581+ boundary := boundaries [i ]
582+ prevBoundary := boundaries [i - 1 ]
583+
584+ // Get the previous loop variable name
585+ prevArrayParts := strings .Split (prevBoundary .ArrayPath , "." )
586+ prevArrayFieldName := prevArrayParts [len (prevArrayParts )- 1 ]
587+ prevLoopVar := generateLoopVariableName (prevArrayFieldName )
588+
589+ // Get the current loop variable name from this boundary's array
590+ arrayParts := strings .Split (boundary .ArrayPath , "." )
591+ arrayFieldName := arrayParts [len (arrayParts )- 1 ]
592+ loopVar := generateLoopVariableName (arrayFieldName )
593+
594+ // Build the array access path from the previous boundary's ItemPath
595+ // This tells us how to get from the previous loop variable to this array
596+ arrayAccessFromPrev := buildFieldAccessPath (prevBoundary .ItemPath )
597+ arrayAccessInLoop := strings .Replace (arrayAccessFromPrev , "resource" , prevLoopVar , 1 )
598+
599+ // Wrap current block in a for loop with nil check
600+ currentBlock = jen .If (jen .Id (arrayAccessInLoop ).Op ("!=" ).Nil ()).Block (
601+ jen .For (
602+ jen .List (jen .Id ("_" ), jen .Id (loopVar )).Op (":=" ).Range ().Op ("*" ).Id (arrayAccessInLoop ),
603+ ).Block (currentBlock ),
493604 )
494605 }
495606
496- return code
607+ // Handle the first/outermost boundary
608+ firstBoundary := boundaries [0 ]
609+ firstArrayAccessPath := buildFieldAccessPath (firstBoundary .ArrayPath )
610+ firstArrayParts := strings .Split (firstBoundary .ArrayPath , "." )
611+ firstArrayFieldName := firstArrayParts [len (firstArrayParts )- 1 ]
612+ firstLoopVar := generateLoopVariableName (firstArrayFieldName )
613+
614+ // Wrap in the outermost nil check and for loop
615+ return jen .If (jen .Id (firstArrayAccessPath ).Op ("!=" ).Nil ()).Block (
616+ jen .For (
617+ jen .List (jen .Id ("_" ), jen .Id (firstLoopVar )).Op (":=" ).Range ().Op ("*" ).Id (firstArrayAccessPath ),
618+ ).Block (currentBlock ),
619+ )
497620}
498621
499622func buildFieldAccessPath (fieldPath string ) string {
@@ -503,11 +626,22 @@ func buildFieldAccessPath(fieldPath string) string {
503626 for i := 0 ; i < len (parts ); i ++ {
504627 part := parts [i ]
505628
506- // Skip "properties" and "items" keywords. Array based indexers are not supported for now
507- if part == "properties" || part == "items" {
629+ // Skip "properties" keyword
630+ if part == "properties" {
508631 continue
509632 }
510633
634+ // Skip "items" only if it's the schema marker (followed by "properties")
635+ // Keep "items" if it's an actual field name (last part or followed by something other than "properties")
636+ if part == "items" {
637+ // Check if this is the schema marker: ".items.properties."
638+ if i + 1 < len (parts ) && parts [i + 1 ] == "properties" {
639+ // This is the schema marker, skip it
640+ continue
641+ }
642+ // Otherwise, it's a field name, keep it
643+ }
644+
511645 // Capitalize the first letter
512646 accessPath = append (accessPath , capitalizeFirst (part ))
513647 }
@@ -522,6 +656,88 @@ func capitalizeFirst(s string) string {
522656 return strings .ToUpper (s [:1 ]) + s [1 :]
523657}
524658
659+ // parseAllArrayBoundaries parses a field path and returns all array boundaries.
660+ // This supports nested arrays by finding all .items.properties. patterns.
661+ // Example: "properties.spec.properties.regions.items.properties.notifications.items.properties.secretRef"
662+ // Returns: [
663+ //
664+ // {ArrayPath: "properties.spec.properties.regions", ItemPath: "properties.notifications"},
665+ // {ArrayPath: "properties.notifications", ItemPath: "properties.secretRef"}
666+ //
667+ // ]
668+ func parseAllArrayBoundaries (fieldPath string ) []ArrayBoundary {
669+ var boundaries []ArrayBoundary
670+
671+ // Split by ".items.properties." to find all array boundaries
672+ const delimiter = ".items.properties."
673+ parts := strings .Split (fieldPath , delimiter )
674+
675+ if len (parts ) <= 1 {
676+ // No arrays found
677+ return nil
678+ }
679+
680+ // Each part except the last represents an array container
681+ // The item path is derived from the next part
682+ for i := 0 ; i < len (parts )- 1 ; i ++ {
683+ arrayPath := parts [i ]
684+
685+ // For nested arrays (i > 0), we need to get just the array field name
686+ // from the previous item path
687+ if i > 0 {
688+ // The arrayPath here is relative to the previous loop variable
689+ // Extract just the array field name
690+ arrayPath = "properties." + extractArrayFieldFromItemPath (parts [i ])
691+ }
692+
693+ // The item path is the next part (after splitting, parts won't contain ".items.")
694+ itemPath := "properties." + parts [i + 1 ]
695+
696+ boundaries = append (boundaries , ArrayBoundary {
697+ ArrayPath : arrayPath ,
698+ ItemPath : itemPath ,
699+ })
700+ }
701+
702+ return boundaries
703+ }
704+
705+ // extractArrayFieldFromItemPath extracts the array field name from an item path
706+ // For "config.properties.notifications", it returns "notifications" (the last field - the array)
707+ // For "notifications", it returns "notifications"
708+ func extractArrayFieldFromItemPath (path string ) string {
709+ // Handle paths like "notifications" or "config.properties.notifications"
710+ path = strings .TrimPrefix (path , "properties." )
711+
712+ // For nested paths like "config.properties.notifications", we want the last segment (the array name)
713+ lastPropertiesIdx := strings .LastIndex (path , ".properties." )
714+ if lastPropertiesIdx >= 0 {
715+ return path [lastPropertiesIdx + len (".properties." ):] // skip ".properties."
716+ }
717+
718+ return path
719+ }
720+
721+ func generateLoopVariableName (arrayFieldName string ) string {
722+ if arrayFieldName == "" {
723+ return "item"
724+ }
725+
726+ name := strings .ToLower (arrayFieldName )
727+
728+ if strings .HasSuffix (name , "ies" ) {
729+ return name [:len (name )- 3 ] + "y"
730+ }
731+ if strings .HasSuffix (name , "ses" ) || strings .HasSuffix (name , "ches" ) || strings .HasSuffix (name , "xes" ) {
732+ return name [:len (name )- 2 ]
733+ }
734+ if strings .HasSuffix (name , "s" ) {
735+ return name [:len (name )- 1 ]
736+ }
737+
738+ return name + "Item"
739+ }
740+
525741// buildNilCheckConditions creates a compound nil check condition for a field access path
526742// based on which segments are required (non-pointer) vs optional (pointer).
527743// Examples:
0 commit comments