Core/Export-TestEnvironmentReport.ps1
|
function Export-TestEnvironmentReport { <# .SYNOPSIS Writes a report built by New-TestEnvironmentReport as JSON, CSV or HTML .DESCRIPTION The one file writer behind every provider's report, so a JSON, CSV or HTML report looks the same whichever directory it describes, and the encoding rules are kept in one place. Every file is UTF-8: the seeded names carry accents and other writing systems on purpose, and the defaults - ASCII from Export-Csv on Windows PowerShell, the code page from Set-Content - would replace them with question marks and report success. JSON is the whole object. CSV is a folder with one file per section, named by the prefix and the section, an empty file for an empty section, so the folder always holds one file per section. HTML is one page with a heading and a table per section. In the CSV and HTML rows a multi-valued column is joined with a semicolon and a nested object is written as compact JSON, because the alternative is the literal text "System.Object[]", which looks like data and is not. .PARAMETER Report The report, as New-TestEnvironmentReport built it .PARAMETER OutputFormat JSON, CSV or HTML .PARAMETER OutputPath The file to write, or for CSV the folder. Created if it does not exist. .PARAMETER FilePrefix What each CSV file name starts with, ahead of the section name .PARAMETER Title The HTML page title and heading. Defaults to "<Provider> Test Environment Report". .PARAMETER Note Lines written under the HTML heading, as text .PARAMETER Warning Lines written under the HTML heading and marked as warnings, for the one number a reader must not miss .OUTPUTS None. The path written is reported through Write-Verbose. .EXAMPLE PS> Export-TestEnvironmentReport -Report $report -OutputFormat CSV -OutputPath ./out -FilePrefix OktaLab Writes OktaLabUsers.csv, OktaLabGroups.csv and so on, one per section. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [PSObject]$Report, [Parameter(Mandatory = $true)] [ValidateSet('JSON', 'CSV', 'HTML')] [string]$OutputFormat, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$OutputPath, [Parameter()] [AllowEmptyString()] [string]$FilePrefix = '', [Parameter()] [ValidateNotNullOrEmpty()] [string]$Title, [Parameter()] [AllowEmptyCollection()] [string[]]$Note = @(), [Parameter()] [AllowEmptyCollection()] [string[]]$Warning = @() ) if (-not $Title) { $Title = '{0} Test Environment Report' -f $Report.Provider } $sections = @($Report.Sections) # A row as a table can hold it: scalars as they are, lists joined, objects as compact JSON. $flatten = { param($row) $flat = [ordered]@{} foreach ($property in $row.PSObject.Properties) { $value = $property.Value if ($null -eq $value -or $value -is [string] -or $value -is [ValueType]) { $flat[$property.Name] = $value } elseif ($value -is [System.Collections.IDictionary] -or $value -is [PSCustomObject]) { $flat[$property.Name] = ($value | ConvertTo-Json -Compress -Depth 5) } elseif ($value -is [System.Collections.IEnumerable]) { $flat[$property.Name] = (@($value | ForEach-Object { [string]$_ }) -join '; ') } else { $flat[$property.Name] = [string]$value } } [PSCustomObject]$flat } $writeUtf8 = { param($path, $text) $directory = Split-Path -Path $path -Parent if ($directory -and -not (Test-Path -LiteralPath $directory)) { $null = New-Item -ItemType Directory -Path $directory -Force } [System.IO.File]::WriteAllBytes($path, [System.Text.Encoding]::UTF8.GetBytes($text)) } switch ($OutputFormat) { 'JSON' { & $writeUtf8 $OutputPath ($Report | ConvertTo-Json -Depth 10) } 'CSV' { if (-not (Test-Path -LiteralPath $OutputPath)) { $null = New-Item -ItemType Directory -Path $OutputPath -Force } foreach ($section in $sections) { $file = Join-Path -Path $OutputPath -ChildPath ('{0}{1}.csv' -f $FilePrefix, $section) $rows = @($Report.$section | ForEach-Object { & $flatten $_ }) if ($rows.Count -gt 0) { $rows | Export-Csv -LiteralPath $file -NoTypeInformation -Encoding UTF8 } else { & $writeUtf8 $file '' } } } 'HTML' { $encode = { param($text) [System.Net.WebUtility]::HtmlEncode([string]$text) } $style = @' <style> body { font-family: Segoe UI, system-ui, sans-serif; margin: 2rem; color: #1f2933; } h1 { border-bottom: 2px solid #005b9f; padding-bottom: .3rem; font-size: 1.4rem; } h2 { margin-top: 2rem; color: #005b9f; font-size: 1.1rem; } p.meta { color: #555; font-size: .9rem; } p.warn { color: #b34700; font-weight: 600; } table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; } th, td { border: 1px solid #d3d8de; padding: .4rem .6rem; text-align: left; font-size: .9rem; } th { background: #eef3f8; } tr:nth-child(even) td { background: #fafbfc; } </style> '@ $meta = New-Object System.Collections.Generic.List[string] if ($Report.Target) { $meta.Add(('Target {0}' -f (& $encode $Report.Target))) } if ($Report.PSObject.Properties['Prefix'] -and $Report.Prefix) { $meta.Add(('Prefix {0}' -f (& $encode $Report.Prefix))) } $meta.Add(('Generated {0}' -f (& $encode $Report.GeneratedOn))) $body = New-Object System.Collections.Generic.List[string] $body.Add(('<h1>{0}</h1>' -f (& $encode $Title))) $body.Add(('<p class="meta">{0}</p>' -f ($meta -join ' · '))) foreach ($line in $Note) { $body.Add(('<p>{0}</p>' -f (& $encode $line))) } foreach ($line in $Warning) { $body.Add(('<p class="warn">{0}</p>' -f (& $encode $line))) } foreach ($section in $sections) { $rows = @($Report.$section | ForEach-Object { & $flatten $_ }) $body.Add(('<h2>{0} ({1})</h2>' -f (& $encode $section), $rows.Count)) # ConvertTo-Html -Fragment encodes its input, so seeded names carrying accented # characters or an ampersand survive rather than corrupting the page. if ($rows.Count -gt 0) { $body.Add((($rows | ConvertTo-Html -Fragment) -join "`n")) } } $html = @( '<!DOCTYPE html>', '<html><head><meta charset="utf-8">', ('<title>{0}</title>' -f (& $encode $Title)), $style, '</head><body>' $body.ToArray() '</body></html>' ) -join "`n" & $writeUtf8 $OutputPath $html } } Write-Verbose "Wrote the $OutputFormat report to $OutputPath" } |