Private/Utils/New-DataFromStructure.ps1

Function New-DataFromStructure {
    <#
    .SYNOPSIS
    Generates complex data structures based on pattern descriptions.
 
    .DESCRIPTION
    This function recursively generates PowerShell data structures (hashtables, PSCustomObjects,
    arrays) based on pattern descriptions created by Get-StructurePattern. It handles complex
    nested structures while preventing infinite recursion through depth limiting.
 
    The function generates arrays with random sizes up to MaxArrayItems, creates objects with
    the same property structure as the original, and ensures proper type preservation including
    array wrapping to prevent PowerShell's array unwrapping behavior.
 
    .PARAMETER Pattern
    A hashtable describing the structure pattern, typically created by Get-StructurePattern.
    Contains type information and nested patterns for complex structures.
 
    .PARAMETER Depth
    Current recursion depth. Used internally for infinite recursion prevention.
    Default value is 0.
 
    .PARAMETER MaxDepth
    Maximum allowed recursion depth. When exceeded, returns "max-depth-reached" string.
    Default value is 10.
 
    .PARAMETER FieldPath
    Current field path in dot notation. Used internally for tracking location
    within nested structures.
 
    .PARAMETER MaxArrayItems
    Maximum number of items to generate in arrays. Arrays will have random sizes
    between 1 and this value (inclusive). Default value is 5.
 
    .PARAMETER RandomGenerator
    A System.Random instance for generating random values and array sizes.
    Using the same seed ensures reproducible results.
 
    .PARAMETER Anonymize
    Indicates whether to generate anonymized values. Passed through to value
    generation functions.
 
    .OUTPUTS
    [object]
    Returns generated data structures:
    - Arrays: Object[] with random number of items
    - Hashtables: Hashtable with same property structure
    - PSCustomObjects: PSCustomObject with same property structure
    - Simple values: Delegates to New-ValueFromPattern
    - Max depth reached: "max-depth-reached" string
 
    .EXAMPLE
    $pattern = @{Type = 'array'; ItemCount = 3; ItemPatterns = @(@{Type = 'string'; Pattern = 'text'})}
    New-DataFromStructure -Pattern $pattern -MaxArrayItems 5
    Returns: Array with 1-5 random text strings
 
    .EXAMPLE
    $pattern = @{Type = 'object'; ObjectType = 'hashtable'; Properties = @{name = @{Type = 'string'}}}
    New-DataFromStructure -Pattern $pattern
    Returns: Hashtable with 'name' property containing generated string
 
    .NOTES
    This is an internal utility function used by the PSTestableData module for generating
    complex test data structures. It maintains proper PowerShell type behavior and prevents
    array unwrapping through careful use of array operators and type casting.
    #>

    [CmdletBinding()]
    Param(
        [hashtable]$Pattern,
        [int]$Depth = 0,
        [int]$MaxDepth = 10,
        [string]$FieldPath = "",
        [int]$MaxArrayItems = 5,
        [System.Random]$RandomGenerator = [System.Random]::new(),
        [bool]$Anonymize = $false
    )

    # Prevent infinite recursion
    if ($Depth -gt $MaxDepth) {
        return "max-depth-reached"
    }

    switch ($Pattern.Type) {
        'array' {
            # Handle empty arrays specifically
            if ($Pattern.ItemCount -eq 0) {
                # Return empty array, force as array type with comma operator
                return ,[array]@()
            }

            # Generate a truly random array size from 1 to MaxArrayItems (inclusive)
            # Custom random function handles inclusive/exclusive properly
            $newCount = $RandomGenerator.Next(1, ($MaxArrayItems + 1))
            $newArray = [System.Collections.ArrayList]::new()

            for ($i = 0; $i -lt $newCount; $i++) {
                # Use patterns from sample items, or create default pattern
                if ($Pattern.ItemPatterns -and $Pattern.ItemPatterns.Count -gt 0) {
                    $patternIndex = $i % $Pattern.ItemPatterns.Count
                    $itemPattern = $Pattern.ItemPatterns[$patternIndex]

                    if ($itemPattern.Type -eq 'object' -or $itemPattern.Type -eq 'array') {
                        $null = $newArray.Add((New-DataFromStructure -Pattern $itemPattern -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath "$FieldPath[]" -MaxArrayItems $MaxArrayItems -RandomGenerator $RandomGenerator -Anonymize $Anonymize))
                    }
                    else {
                        $null = $newArray.Add((New-ValueFromPattern -Pattern $itemPattern -RandomGenerator $RandomGenerator -Anonymize $Anonymize))
                    }
                }
                else {
                    # No patterns available, create a default string item
                    $defaultPattern = @{ Type = 'string'; Pattern = 'text'; Length = 10 }
                    $null = $newArray.Add((New-ValueFromPattern -Pattern $defaultPattern -RandomGenerator $RandomGenerator -Anonymize $Anonymize))
                }
            }

            # Convert back to array and ensure it stays as array even if empty or single item
            # Force conversion to Object[] to prevent unwrapping during return
            return [object[]]$newArray.ToArray()
        }
        'object' {
            if ($Pattern.ObjectType -eq 'hashtable') {
                $newObject = @{}
                foreach ($propName in $Pattern.Properties.Keys) {
                    $propPattern = $Pattern.Properties[$propName]
                    $childPath = if ($FieldPath) { "$FieldPath.$propName" } else { $propName }

                    if ($propPattern.Type -eq 'object' -or $propPattern.Type -eq 'array') {
                        $value = New-DataFromStructure -Pattern $propPattern -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath $childPath -MaxArrayItems $MaxArrayItems -RandomGenerator $RandomGenerator -Anonymize $Anonymize
                        # Ensure arrays remain arrays even if they have single items
                        if ($propPattern.Type -eq 'array') {
                            # Force array type preservation - always ensure array context
                            $newObject[$propName] = @($value)
                        } else {
                            $newObject[$propName] = $value
                        }
                    }
                    else {
                        $newObject[$propName] = New-ValueFromPattern -Pattern $propPattern -RandomGenerator $RandomGenerator -Anonymize $Anonymize
                    }
                }
                return $newObject
            }
            else {
                $newObject = [PSCustomObject]@{}
                foreach ($propName in $Pattern.Properties.Keys) {
                    $propPattern = $Pattern.Properties[$propName]
                    $childPath = if ($FieldPath) { "$FieldPath.$propName" } else { $propName }

                    if ($propPattern.Type -eq 'object' -or $propPattern.Type -eq 'array') {
                        $value = New-DataFromStructure -Pattern $propPattern -Depth ($Depth + 1) -MaxDepth $MaxDepth -FieldPath $childPath -MaxArrayItems $MaxArrayItems -RandomGenerator $RandomGenerator -Anonymize $Anonymize
                        # Ensure arrays remain arrays even if they have single items
                        if ($propPattern.Type -eq 'array') {
                            # Force array type preservation for PSCustomObjects - always ensure array context
                            $arrayValue = @($value)
                            $newObject | Add-Member -NotePropertyName $propName -NotePropertyValue $arrayValue
                        } else {
                            $newObject | Add-Member -NotePropertyName $propName -NotePropertyValue $value
                        }
                    }
                    else {
                        $newObject | Add-Member -NotePropertyName $propName -NotePropertyValue (New-ValueFromPattern -Pattern $propPattern -RandomGenerator $RandomGenerator -Anonymize $Anonymize)
                    }
                }
                return $newObject
            }
        }
        default {
            return New-ValueFromPattern -Pattern $Pattern -RandomGenerator $RandomGenerator -Anonymize $Anonymize
        }
    }
}