Public/New-ConfigurationFromSample.ps1

function New-ConfigurationFromSample {
    <#
    .SYNOPSIS
        Analyzes an object and creates a configuration template for data generation.
 
    .DESCRIPTION
        This function takes a sample object and generates a configuration structure that can be
        used to control how data is generated by data generation functions. The configuration
        maps every field in the object with an Action property that determines how the field
        should be handled: Preserve (keep original), Anonymize (scramble while preserving format),
        or Randomize (generate new random value).
 
        The output configuration is designed to be easily edited by users to customize how each
        field should be handled during data generation.
 
    .PARAMETER SampleObject
        The sample object to analyze and create a configuration from. Can be hashtables,
        PSCustomObjects, arrays, or any structured data.
 
    .PARAMETER DefaultAction
        Default action for all fields. Valid values: 'Preserve', 'Anonymize', 'Randomize'.
        Default is 'Preserve'.
 
    .PARAMETER DefaultArrayCount
        Default number of items to generate for arrays. Default is 3.
 
    .PARAMETER AnonymizePatterns
        Array of field path patterns that should default to Action = 'Anonymize'.
        Supports wildcards. Examples: 'password', 'email', '*.secret'
 
    .PARAMETER MaxDepth
        Maximum depth for recursion when analyzing nested structures. Default is 10.
 
    .PARAMETER OutputPath
        Path to save the configuration as a PowerShell script file (.ps1) that can be dot-sourced later.
        The file will contain a $Config variable with the configuration hashtable that can be easily
        edited and reused.
 
    .PARAMETER PassThru
        When used with OutputPath, returns the configuration object in addition to saving it to a file.
        Without this switch, only the file path is returned when OutputPath is specified.
 
    .EXAMPLE
        $sample = @{
            name = "John Doe"
            age = 30
            items = @("item1", "item2")
        }
        $config = New-ConfigurationFromSample -SampleObject $sample
 
        Generates a configuration template for the sample object.
 
    .EXAMPLE
        $yamlData = ConvertFrom-Yaml $yamlString
        $config = New-ConfigurationFromSample -SampleObject $yamlData -DefaultAction 'Anonymize' -AnonymizePatterns @('*.uid', '*.secret')
 
        Generates a configuration with all fields defaulting to Anonymize action, with specific patterns for sensitive fields.
 
    .EXAMPLE
        $sample | New-ConfigurationFromSample -OutputPath '.\config.ps1'
 
        Saves the configuration to a file. Later you can load it with: . .\config.ps1
        The configuration is available in the $Config variable.
 
    .EXAMPLE
        $config = $sample | New-ConfigurationFromSample -OutputPath '.\config.ps1' -PassThru
 
        Saves the configuration to a file and also returns it to the pipeline.
 
    .OUTPUTS
        [hashtable] - Configuration structure with field options
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]$SampleObject,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Preserve', 'Anonymize', 'Randomize')]
        [string]$DefaultAction = 'Preserve',

        [Parameter(Mandatory = $false)]
        [int]$DefaultArrayCount = 3,

        [Parameter(Mandatory = $false)]
        [string[]]$AnonymizePatterns = @(),

        [Parameter(Mandatory = $false)]
        [int]$MaxDepth = 10,

        [Parameter(Mandatory = $false)]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    begin {
        # Helper function to convert hashtable to PowerShell syntax for saving to file
        function ConvertTo-PowerShellSyntax {
            param(
                [hashtable]$Hashtable,
                [int]$IndentLevel = 0
            )

            $indent = ' ' * $IndentLevel
            $nextIndent = ' ' * ($IndentLevel + 1)
            $lines = @()
            $lines += '@{'

            foreach ($key in $Hashtable.Keys | Sort-Object) {
                $value = $Hashtable[$key]
                $quotedKey = if ($key -match '^[a-zA-Z_][a-zA-Z0-9_]*$') { $key } else { "'$key'" }

                if ($value -is [hashtable]) {
                    $nestedHash = ConvertTo-PowerShellSyntax -Hashtable $value -IndentLevel ($IndentLevel + 1)
                    $lines += "$nextIndent$quotedKey = $nestedHash"
                }
                elseif ($value -is [array]) {
                    $arrayItems = @()
                    foreach ($item in $value) {
                        if ($item -is [hashtable]) {
                            $arrayItems += ConvertTo-PowerShellSyntax -Hashtable $item -IndentLevel ($IndentLevel + 2)
                        }
                        else {
                            $arrayItems += "'$item'"
                        }
                    }
                    $lines += "$nextIndent$quotedKey = @($($arrayItems -join ', '))"
                }
                elseif ($value -is [bool]) {
                    $lines += "$nextIndent$quotedKey = `$$value"
                }
                elseif ($value -is [string]) {
                    $escapedValue = $value -replace "'", "''"
                    $lines += "$nextIndent$quotedKey = '$escapedValue'"
                }
                elseif ($null -eq $value) {
                    $lines += "$nextIndent$quotedKey = `$null"
                }
                else {
                    $lines += "$nextIndent$quotedKey = $value"
                }
            }

            $lines += "$indent}"
            return $lines -join "`n"
        }

        # Helper function to determine field type from value
        function Get-FieldType {
            param([object]$Value)

            if ($null -eq $Value) {
                return 'null'
            }

            $type = $Value.GetType()

            # Check for string patterns first
            if ($Value -is [string]) {
                if ($Value -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
                    return 'guid'
                }
                elseif ($Value -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') {
                    return 'datetime'
                }
                else {
                    return 'string'
                }
            }
            elseif ($Value -is [int] -or $Value -is [int32]) {
                return 'int'
            }
            elseif ($Value -is [long] -or $Value -is [int64]) {
                return 'long'
            }
            elseif ($Value -is [double] -or $Value -is [decimal]) {
                return 'double'
            }
            elseif ($Value -is [bool]) {
                return 'bool'
            }
            elseif ($Value -is [datetime]) {
                return 'datetime'
            }
            elseif ($Value -is [guid]) {
                return 'guid'
            }
            elseif ($type.IsArray -or ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [hashtable] -and $Value -isnot [PSCustomObject])) {
                return 'array'
            }
            elseif ($Value -is [hashtable] -or $Value -is [PSCustomObject]) {
                return 'object'
            }
            else {
                return 'string'
            }
        }

        # Helper function to check if field matches patterns
        function Test-MatchesPattern {
            param(
                [string]$FieldPath,
                [string[]]$Patterns
            )

            if ($Patterns.Count -eq 0) {
                return $false
            }

            foreach ($pattern in $Patterns) {
                # Try exact match using PowerShell -like operator
                try {
                    if ($FieldPath -like $pattern) {
                        return $true
                    }
                }
                catch {
                    # If wildcard pattern is invalid, try exact match
                    if ($FieldPath -eq $pattern) {
                        return $true
                    }
                }

                # Also check if the pattern matches the field name at the end of the path
                # This allows "apiVersion" to match "items.*.apiVersion"
                if (-not $pattern.Contains('*') -and -not $pattern.Contains('.')) {
                    # Simple field name without wildcards or dots
                    $pathSegments = $FieldPath -split '\.'
                    if ($pathSegments[-1] -eq $pattern) {
                        return $true
                    }
                }
            }

            return $false
        }

        # Main recursive function to build configuration
        function Build-Configuration {
            param(
                [object]$Object,
                [int]$Depth = 0,
                [string]$FieldPath = "",
                [hashtable]$Context = @{}
            )

            # Prevent infinite recursion
            if ($Depth -gt $MaxDepth) {
                return @{
                    Action = $DefaultAction
                    Type = 'string'
                }
            }

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

            $fieldType = Get-FieldType -Value $Object
            $fieldAction = if ($AnonymizePatterns.Count -gt 0 -and (Test-MatchesPattern -FieldPath $FieldPath -Patterns $AnonymizePatterns)) {
                'Anonymize'
            } else {
                $DefaultAction
            }

            if ($fieldType -eq 'array') {
                # Handle arrays
                $items = @($Object)
                $itemConfig = @{
                    Action = $fieldAction
                    Type = 'array'
                    ArrayCount = [math]::Min($items.Count, $DefaultArrayCount)
                }

                # Analyze first item to determine array item structure
                if ($items.Count -gt 0 -and $null -ne $items[0]) {
                    $firstItemType = Get-FieldType -Value $items[0]

                    if ($firstItemType -eq 'object') {
                        # Array of objects - analyze structure
                        # Use the parent path without [*] suffix for matching patterns inside array items
                        # This allows patterns like "items.apiVersion" or "*.apiVersion" to work
                        $childPath = if ($FieldPath) { "$FieldPath.*" } else { "*" }
                        $itemConfig.ItemStructure = Build-Configuration -Object $items[0] -Depth ($Depth + 1) -FieldPath $childPath -Context $Context
                    }
                    else {
                        # Array of primitives
                        $childPath = if ($FieldPath) { "$FieldPath[*]" } else { "[*]" }
                        $itemConfig.ItemType = $firstItemType
                        $itemAction = if ($AnonymizePatterns.Count -gt 0 -and (Test-MatchesPattern -FieldPath $childPath -Patterns $AnonymizePatterns)) {
                            'Anonymize'
                        } else {
                            $DefaultAction
                        }
                        $itemConfig.ItemAction = $itemAction
                    }
                }

                return $itemConfig
            }
            elseif ($fieldType -eq 'object') {
                # Handle objects (hashtables/PSCustomObjects)
                $config = @{}

                if ($Object -is [hashtable]) {
                    foreach ($key in $Object.Keys) {
                        $childPath = if ($FieldPath) { "$FieldPath.$key" } else { $key }
                        $config[$key] = Build-Configuration -Object $Object[$key] -Depth ($Depth + 1) -FieldPath $childPath -Context $Context
                    }
                }
                else {
                    # PSCustomObject
                    foreach ($prop in $Object.PSObject.Properties) {
                        $childPath = if ($FieldPath) { "$FieldPath.$($prop.Name)" } else { $prop.Name }
                        $config[$prop.Name] = Build-Configuration -Object $prop.Value -Depth ($Depth + 1) -FieldPath $childPath -Context $Context
                    }
                }

                return $config
            }
            else {
                # Primitive values
                return @{
                    Action = $fieldAction
                    Type = $fieldType
                }
            }
        }
    }

    process {
        $context = @{}
        $config = Build-Configuration -Object $SampleObject -Depth 0 -FieldPath "" -Context $context

        # Store config for end block
        if (-not $script:generatedConfigs) {
            $script:generatedConfigs = @()
        }
        $script:generatedConfigs += $config
    }

    end {
        # If only one config was generated, return it directly (not as array)
        $finalConfig = if ($script:generatedConfigs.Count -eq 1) {
            $script:generatedConfigs[0]
        } else {
            $script:generatedConfigs
        }

        # Clean up script variable
        Remove-Variable -Name generatedConfigs -Scope Script -ErrorAction SilentlyContinue

        # Handle OutputPath if specified
        if ($OutputPath) {
            $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)

            # Ensure directory exists
            $directory = Split-Path -Path $resolvedPath -Parent
            if ($directory -and -not (Test-Path -Path $directory)) {
                New-Item -Path $directory -ItemType Directory -Force | Out-Null
            }

            # Generate PowerShell script content
            $scriptContent = @"
# Configuration generated by New-ConfigurationFromSample
# Generated on: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
#
# Usage:
# Dot-source this file to load the configuration into the `$Config variable:
# . '$resolvedPath'
#
# Or assign it to a custom variable:
# `$myConfig = . '$resolvedPath'
#
# The configuration can be edited manually and then reloaded.
 
`$Config = $(ConvertTo-PowerShellSyntax -Hashtable $finalConfig -IndentLevel 0)
 
# Return the configuration when dot-sourced
return `$Config
"@


            # Write to file
            Set-Content -Path $resolvedPath -Value $scriptContent -Encoding UTF8
            Write-Verbose "Configuration saved to: $resolvedPath"

            # Return based on PassThru switch
            if ($PassThru) {
                return $finalConfig
            } else {
                return $resolvedPath
            }
        }
        else {
            return $finalConfig
        }
    }
}