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 } } } |