Public/ConvertFrom-DhYaml.ps1

function ConvertFrom-DhYaml {
    <#
    .SYNOPSIS
        Generate a runnable PowerShell script from a YAML (or JSON) dashboard
        layout spec.

    .DESCRIPTION
        Reads a declarative layout file (the schema emitted by ConvertTo-DhYaml,
        or hand-written) and produces a .ps1 script of New-DhDashboard / Add-Dh*
        / Export-DhDashboard calls, in dependency-correct order (tables are
        declared before the blocks that reference them).

        The generated parameters are validated against the LIVE cmdlet metadata
        (Get-Command), so only real parameters are emitted and switch vs. bool is
        handled correctly. Keys that are not parameters (e.g. a `dataRows` count)
        are skipped.

        Table data: a `dataFile` key becomes `-Data (Import-Csv '<file>')`; an
        inline `data` array becomes `-Data @(...)`; otherwise a
        `-Data @() # TODO` placeholder is emitted for you to fill in.

        This tool requires the core DashHtml module to be available (it reads the
        cmdlet metadata). It does NOT run the generated script.

    .PARAMETER Path
        Path to the .yaml / .yml / .json layout file.

    .PARAMETER OutFile
        Optional path to write the generated .ps1. When omitted, the script text
        is returned.

    .PARAMETER OutputPath
        The -OutputPath baked into the generated Export-DhDashboard call.
        Defaults to '.\dashboard.html'.

    .EXAMPLE
        ConvertFrom-DhYaml -Path infra.yaml -OutFile build-infra.ps1
        .\build-infra.ps1 # run it to produce the HTML
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Path,
        [string] $OutFile,
        [string] $OutputPath = '.\dashboard.html'
    )

    if (-not (Test-Path -LiteralPath $Path)) { throw "ConvertFrom-DhYaml: file not found: $Path" }
    $model = ConvertFrom-DhYamlText (Get-Content -LiteralPath $Path -Raw)
    if (-not (Test-DhYamlMap $model)) { throw "ConvertFrom-DhYaml: top-level document must be a mapping." }

    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.AppendLine('#Requires -Version 7.0')
    [void]$sb.AppendLine('# Generated by ConvertFrom-DhYaml from ' + (Split-Path $Path -Leaf))
    [void]$sb.AppendLine('Import-Module DashHtml -ErrorAction Stop')
    [void]$sb.AppendLine('')

    # ---- New-DhDashboard ----
    $ndArgs = [System.Collections.Generic.List[string]]::new()
    $ndParams = Get-DhCmdletParamMeta 'New-DhDashboard'
    foreach ($k in $model.Keys) {
        if ($k -in @('summary','tables','blocks','globalFilters','alertBanners','links')) { continue }
        if ($k -eq 'autoRefresh') {
            $ar = $model[$k]
            # int seconds (from ConvertTo-DhYaml) -> 'Ns'; a string like '5m' (from a script) -> as-is
            $arStr = if ($ar -is [string]) { $ar } else { "$([int]$ar)s" }
            [void]$ndArgs.Add("-AutoRefresh '$arStr'"); continue
        }
        $arg = Format-DhArg -ParamMeta $ndParams -Key $k -Value $model[$k]
        if ($arg) { [void]$ndArgs.Add($arg) }
    }
    [void]$sb.AppendLine('$report = New-DhDashboard ' + ($ndArgs -join ' '))
    [void]$sb.AppendLine('')

    # ---- Global summary (declared before tables so it renders at the top) ----
    if ($model.Contains('summary') -and $model['summary']) {
        $s = $model['summary']
        $callArgs = [System.Collections.Generic.List[string]]::new(); [void]$callArgs.Add('-Report $report')
        $meta = Get-DhCmdletParamMeta 'Add-DhSummary'
        foreach ($k in $s.Keys) {
            $arg = Format-DhArg -ParamMeta $meta -Key $k -Value $s[$k]
            if ($arg) { [void]$callArgs.Add($arg) }
        }
        [void]$sb.AppendLine('Add-DhSummary ' + ($callArgs -join ' '))
        [void]$sb.AppendLine('')
    }

    # ---- Tables (BEFORE blocks) ----
    if ($model.Contains('tables')) {
        $tblMeta = Get-DhCmdletParamMeta 'Add-DhTable'
        foreach ($t in @($model['tables'])) {
            $callArgs = [System.Collections.Generic.List[string]]::new(); [void]$callArgs.Add('-Report $report')
            foreach ($k in $t.Keys) {
                if ($k -in @('data','dataFile','dataRows')) { continue }
                $mapKey = if ($k -ieq 'id') { 'TableId' } else { $k }
                $arg = Format-DhArg -ParamMeta $tblMeta -Key $mapKey -Value $t[$k]
                if ($arg) { [void]$callArgs.Add($arg) }
            }
            # data resolution
            if     ($t.Contains('dataFile') -and $t['dataFile']) { [void]$callArgs.Add("-Data (Import-Csv '$($t['dataFile'])')") }
            elseif ($t.Contains('data')     -and $t['data'])     { [void]$callArgs.Add('-Data ' + (ConvertTo-DhPsLiteral @($t['data']))) }
            else {
                $cntNote = if ($t.Contains('dataRows')) { " # TODO: supply data ($($t['dataRows']) rows in the source)" } else { ' # TODO: supply -Data' }
                [void]$callArgs.Add('-Data @()' + $cntNote)
            }
            [void]$sb.AppendLine('Add-DhTable ' + ($callArgs -join ' '))
        }
        [void]$sb.AppendLine('')
    }

    # ---- Blocks ----
    if ($model.Contains('blocks')) {
        foreach ($b in @($model['blocks'])) {
            $type = [string]$b['type']
            $cmd  = $script:DhTypeToCmdlet[$type.ToLower()]
            if (-not $cmd) { [void]$sb.AppendLine("# (skipped) unknown block type '$type'"); continue }
            $meta = Get-DhCmdletParamMeta $cmd
            $callArgs = [System.Collections.Generic.List[string]]::new(); [void]$callArgs.Add('-Report $report')
            foreach ($k in $b.Keys) {
                if ($k -ieq 'type') { continue }
                $arg = Format-DhArg -ParamMeta $meta -Key $k -Value $b[$k]
                if ($arg) { [void]$callArgs.Add($arg) }
            }
            [void]$sb.AppendLine("$cmd " + ($callArgs -join ' '))
        }
        [void]$sb.AppendLine('')
    }

    # ---- Global filters / alert banners / links ----
    foreach ($section in @(
        @{ Key='globalFilters'; Cmd='Add-DhGlobalFilter' },
        @{ Key='alertBanners';  Cmd='Add-DhAlertBanner'  },
        @{ Key='links';         Cmd='Set-DhTableLink'    }
    )) {
        if (-not $model.Contains($section.Key)) { continue }
        $meta = Get-DhCmdletParamMeta $section.Cmd
        if (-not $meta) { continue }
        foreach ($item in @($model[$section.Key])) {
            $callArgs = [System.Collections.Generic.List[string]]::new(); [void]$callArgs.Add('-Report $report')
            foreach ($k in $item.Keys) {
                $arg = Format-DhArg -ParamMeta $meta -Key $k -Value $item[$k]
                if ($arg) { [void]$callArgs.Add($arg) }
            }
            [void]$sb.AppendLine("$($section.Cmd) " + ($callArgs -join ' '))
        }
        [void]$sb.AppendLine('')
    }

    [void]$sb.AppendLine("Export-DhDashboard -Report `$report -OutputPath '$OutputPath' -Force")

    $script = $sb.ToString()
    if ($OutFile) {
        Set-Content -Path $OutFile -Value $script -Encoding UTF8
        Write-Verbose "ConvertFrom-DhYaml: wrote $OutFile"
    } else {
        $script
    }
}

function Get-DhCmdletParamMeta {
    <# Returns the cmdlet's parameter metadata dictionary, or $null if absent. #>
    param([string] $CmdletName)
    $cmd = Get-Command $CmdletName -ErrorAction SilentlyContinue
    if (-not $cmd) { return $null }
    return $cmd.Parameters
}

function Format-DhArg {
    <#
    .SYNOPSIS Render ' -Param <literal>' for a spec key IF the cmdlet has a
               matching parameter; '' otherwise. Handles switch vs bool.
    #>

    param([object] $ParamMeta, [string] $Key, [object] $Value)
    if (-not $ParamMeta) { return '' }
    # case-insensitive param lookup
    $paramName = $null
    foreach ($p in $ParamMeta.Keys) { if ($p -ieq $Key) { $paramName = $p; break } }
    if (-not $paramName) { return '' }
    if ($paramName -in $script:DhCommonParams) { return '' }

    $ptype = $ParamMeta[$paramName].ParameterType
    if ($ptype -eq [System.Management.Automation.SwitchParameter]) {
        if ($Value -is [bool]) { return $(if ($Value) { "-$paramName" } else { '' }) }
        return "-$paramName"
    }
    return "-$paramName $(ConvertTo-DhPsLiteral $Value)"
}