Private/Utils/Get-StructurePattern.ps1

Function Get-StructurePattern {
    <#
    .SYNOPSIS
    Analyzes the structure of complex objects to create a pattern description.
 
    .DESCRIPTION
    This function recursively analyzes PowerShell objects (hashtables, PSCustomObjects, arrays)
    to create a comprehensive pattern description that can be used to generate similar test data.
    It identifies object types, property structures, array characteristics, and applies
    preservation rules for specified field patterns.
 
    The function handles infinite recursion prevention through depth limiting and provides
    detailed analysis of nested structures including arrays with item patterns and objects
    with property patterns.
 
    .PARAMETER Object
    The object to analyze. Can be hashtables, PSCustomObjects, arrays, or simple values.
 
    .PARAMETER Depth
    Current recursion depth. Used internally for infinite recursion prevention.
    Default value is 0.
 
    .PARAMETER MaxDepth
    Maximum allowed recursion depth. When exceeded, returns a fallback pattern.
    Default value is 10.
 
    .PARAMETER FieldPath
    Current field path in dot notation. Used internally for building hierarchical
    field paths and applying preservation patterns.
 
    .PARAMETER PreservePatterns
    Array of field path patterns that should be marked for preservation.
    Supports wildcard patterns for matching multiple fields.
 
    .OUTPUTS
    [hashtable]
    Returns a hashtable describing the structure:
    - For objects: Type, ObjectType, Properties (nested patterns)
    - For arrays: Type, ItemCount, SampleSize, ItemPatterns
    - For simple values: Delegates to Get-ValuePattern
 
    .EXAMPLE
    $obj = @{name = "John"; age = 30}
    Get-StructurePattern -Object $obj -PreservePatterns @()
    Returns pattern describing hashtable with string and integer properties
 
    .EXAMPLE
    $arr = @("item1", "item2", "item3")
    Get-StructurePattern -Object $arr -PreservePatterns @()
    Returns pattern describing array with string item patterns
 
    .EXAMPLE
    $obj = @{apiVersion = "v1"; data = "sensitive"}
    Get-StructurePattern -Object $obj -PreservePatterns @("apiVersion")
    Returns pattern with apiVersion marked for preservation
 
    .NOTES
    This is an internal utility function used by the PSTestableData module for analyzing
    complex data structures. It limits array analysis to the first 3 items for performance
    while maintaining representative patterns.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    Param(
        [object]$Object,
        [int]$Depth = 0,
        [int]$MaxDepth = 10,
        [string]$FieldPath = "",
        [string[]]$PreservePatterns = @()
    )

    # Prevent infinite recursion
    if ($Depth -gt $MaxDepth) {
        return @{ Type = 'string'; Pattern = 'text'; Length = 10 }
    }

    if ($null -eq $Object) {
        return @{ Type = 'null' }
    }

    $type = $Object.GetType()

    if ($Object -is [hashtable] -or $Object -is [PSCustomObject]) {
        # Object pattern - handle this first before IEnumerable check
        $properties = @{}

        if ($Object -is [hashtable]) {
            foreach ($key in $Object.Keys) {
                $childPath = if ($FieldPath) { "$FieldPath.$key" } else { $key }
                $properties[$key] = Get-StructurePattern -Object $Object[$key] -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath $childPath -PreservePatterns $PreservePatterns
            }
        }
        else {
            foreach ($prop in $Object.PSObject.Properties) {
                $childPath = if ($FieldPath) { "$FieldPath.$($prop.Name)" } else { $prop.Name }
                $properties[$prop.Name] = Get-StructurePattern -Object $prop.Value -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath $childPath -PreservePatterns $PreservePatterns
            }
        }

        return @{
            Type = 'object'
            Properties = $properties
            ObjectType = if ($Object -is [hashtable]) { 'hashtable' } else { 'pscustomobject' }
        }
    }
    elseif ($type.IsArray -or ($Object -is [System.Collections.IEnumerable] -and $Object -isnot [string] -and $Object -isnot [hashtable] -and $Object -isnot [PSCustomObject])) {
        # Array pattern - limit analysis to first 3 items to prevent excessive processing
        $items = @($Object)
        $itemPatterns = @()
        $maxItems = [math]::Min($items.Count, 3)

        for ($i = 0; $i -lt $maxItems; $i++) {
            $item = $items[$i]
            if ($item -is [hashtable] -or $item -is [PSCustomObject]) {
                # Use wildcard notation for array items to enable pattern matching like "items.*.apiVersion" or "*.kind"
                $arrayItemPath = if ($FieldPath) { "$FieldPath.*" } else { "*" }
                $itemPatterns += Get-StructurePattern -Object $item -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath $arrayItemPath -PreservePatterns $PreservePatterns
            }
            else {
                $pattern = Get-ValuePattern -Value $item
                # Use wildcard notation for array item values too
                $arrayItemPath = if ($FieldPath) { "$FieldPath.*" } else { "*" }
                $pattern.PreserveField = Test-PreserveField -FieldPath $arrayItemPath -PreservePatterns $PreservePatterns
                # Store original value if field should be preserved
                if ($pattern.PreserveField) {
                    $pattern.OriginalValue = $item
                }
                $itemPatterns += $pattern
            }
        }

        return @{
            Type = 'array'
            ItemCount = $items.Count
            ItemPatterns = $itemPatterns
            SampleSize = $maxItems
        }
    }
    else {
        # Primitive value
        $pattern = Get-ValuePattern -Value $Object
        # Mark if this field should be preserved from anonymization
        $pattern.PreserveField = Test-PreserveField -FieldPath $FieldPath -PreservePatterns $PreservePatterns
        # Store original value if field should be preserved
        if ($pattern.PreserveField) {
            $pattern.OriginalValue = $Object
        }
        return $pattern
    }
}