Noveris.Runner.psm1

<#
#>


########
# Global settings
$ErrorActionPreference = "Stop"
$InformationPreference = "Continue"
Set-StrictMode -Version 2

Class RunnerSection
{
    [string]$Name
    [string]$Description
    [ScriptBlock]$Script
    $Data

    RunnerSection([string]$name, [string]$description, [ScriptBlock]$script, [PSObject]$data)
    {
        $this.Name = $name
        $this.Description = $description
        $this.Script = $script
        $this.Data = $data
    }
}

class RunnerSectionContent
{
    [string]$Name
    [string]$Description
    [System.Collections.ArrayList]$Content

    RunnerSectionContent([string]$Name, [string]$Description)
    {
        $this.Name = $name
        $this.Description = $description
        $this.Content = New-Object 'System.Collections.ArrayList'
    }
}

Class RunnerFormatTable
{
    $Content
}

Class RunnerContext
{
    [System.Collections.Generic.List[RunnerSection]]$Entries

    RunnerContext()
    {
        $this.Entries = New-Object 'System.Collections.Generic.List[RunnerSection]'
    }
}

enum RunnerStatus
{
    None = 0
    Info
    Warning
    Error
    InternalError
}

<#
#>

Class RunnerNotice
{
    [RunnerStatus]$Status
    [string]$Description

    RunnerNotice([RunnerStatus]$status, [string]$description)
    {
        $this.Status = $status
        $this.Description = $description
    }

    [string] ToString()
    {
        return ("{0}: {1}" -f $this.Status.ToString().ToUpper(), $this.Description)
    }
}

<#
#>

Function New-RunnerNotice
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param(
        [Parameter(mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Description,

        [Parameter(mandatory=$false)]
        [ValidateNotNull()]
        [RunnerStatus]$Status = [RunnerStatus]::None
    )

    process
    {
        $notice = New-Object RunnerNotice -ArgumentList $Status, $Description

        $notice
    }
}

<#
#>

Function New-RunnerFormatTable
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param(
        [Parameter(mandatory=$true)]
        [ValidateNotNull()]
        $Content
    )

    process
    {
        $format = New-Object 'RunnerFormatTable'
        $format.Content = $Content

        $format
    }
}

<#
#>

Function New-RunnerContext
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType('RunnerContext')]
    param(
    )

    process
    {
        $obj = New-Object RunnerContext

        $obj
    }
}

<#
#>

Function Add-RunnerContextSection
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline)]
        [ValidateNotNull()]
        [RunnerContext]$Context,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [AllowEmptyString()]
        [string]$Description = "",

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [ScriptBlock]$Script,

        [Parameter(Mandatory=$false)]
        [AllowNull()]
        $Data = $null
    )

    process
    {
        # Add the script to the list of scripts to process
        $entry = New-Object 'RunnerSection' -ArgumentList $Name, $Description, $Script, $Data
        $Context.Entries.Add($entry) | Out-Null
    }
}

<#
#>

Function Invoke-RunnerContext
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [RunnerContext]$Context
    )

    process
    {
        $Context.Entries | ForEach-Object {
            $entry = $_

            # Output a section format object
            $content = New-Object 'RunnerSectionContent' -ArgumentList $entry.Name, $entry.Description

            Invoke-Command -NoNewScope {
                # Run the script block
                try {
                    ForEach-Object -InputObject $entry.Data, -Process $entry.Script
                } catch {
                    New-RunnerNotice -Status InternalError -Description "Error running script: $_"
                }
            } *>&1 | ForEach-Object {
                $content.Content.Add($_) | Out-Null
            }

            $content
        }
    }
}

<#
#>

Function Format-RunnerContentAsHtml
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline)]
        [ValidateNotNull()]
        [RunnerSectionContent]$Section,

        [Parameter(Mandatory=$false)]
        [AllowEmptyString()]
        [ValidateNotNull()]
        [string]$Title = "",

        [Parameter(Mandatory=$false)]
        [bool]$DecodeHtml = $true
    )

    begin
    {
        # Collection of all notices across all sections
        $allNotices = [ordered]@{}

        $allSectionContent = New-Object 'System.Collections.ArrayList'

        # Html preamble
        "<!DOCTYPE html PUBLIC `"-//W3C//DTD XHTML 1.0 Strict//EN`" `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd`">"
        "<html xmlns=`"http://www.w3.org/1999/xhtml`">"
        "<head>"
        "<title>$Title</title>"
        "<style>"
        "table {"
        " font-family: Arial, Helvetica, sans-serif;"
        " border-collapse: collapse;"
        " width: 100%;"
        "}"
        "td, th {"
        " border: 1px solid #ddd;"
        " padding: 8px;"
        "}"
        "tr:nth-child(even){background-color: #f2f2f2;}"
        "tr:hover {background-color: #ddd;}"
        "th {"
        " padding-top: 12px;"
        " padding-bottom: 12px;"
        " text-align: left;"
        " background-color: #04AA6D;"
        " color: white;"
        "}"
        "</style>"
        "</head><body>"
    }

    process
    {
        # Generate string content for this section
        $sectionContent = & {
            $notices = New-Object 'System.Collections.Generic.List[string]'

            # Display section heading
            ("<b>Section: {0}</b><br>" -f $Section.Name)
            ("<i>{0}</i><br><p />" -f $Section.Description)

            $output = $Section.Content | ForEach-Object {

                # Default message to pass on in pipeline
                $msg = $_

                # Check if it is a string or status object
                if ([RunnerNotice].IsAssignableFrom($msg.GetType()))
                {
                    [RunnerNotice]$notice = $_
                    $noticeStr = $notice.ToString()
                    $notices.Add($noticeStr) | Out-Null

                    # Only add Notices that are issues to the all notices list
                    if ($notice.Status -eq [RunnerStatus]::Warning -or $notice.Status -eq [RunnerStatus]::Error -or
                        $notice.Status -eq [RunnerStatus]::InternalError)
                    {
                        if ($null -eq $allNotices[$Section.Name])
                        {
                            $allNotices[$Section.Name] = New-Object 'System.Collections.Generic.List[string]'
                        }

                        $allNotices[$Section.Name].Add($noticeStr) | Out-Null
                    }

                    # Alter message to notice string representation
                    $msg = $noticeStr
                }

                if ([System.Management.Automation.InformationRecord].IsAssignableFrom($_.GetType()))
                {
                    $msg = ("INFO: {0}" -f $_.ToString())
                }
                elseif ([System.Management.Automation.VerboseRecord].IsAssignableFrom($_.GetType()))
                {
                    $msg = ("VERBOSE: {0}" -f $_.ToString())
                }
                elseif ([System.Management.Automation.ErrorRecord].IsAssignableFrom($_.GetType()))
                {
                    $msg = ("ERROR: {0}" -f $_.ToString())
                }
                elseif ([System.Management.Automation.DebugRecord].IsAssignableFrom($_.GetType()))
                {
                    $msg = ("DEBUG: {1}" -f $_.ToString())
                }
                elseif ([System.Management.Automation.WarningRecord].IsAssignableFrom($_.GetType()))
                {
                    $msg = ("WARNING: {1}" -f $_.ToString())
                }

                if ([RunnerFormatTable].IsAssignableFrom($msg.GetType()))
                {
                    $msg = $msg.Content | ConvertTo-Html -As Table -Fragment
                }

                if ([string].IsAssignableFrom($msg.GetType()))
                {
                    $msg += "<br>"
                    if ($DecodeHtml)
                    {
                        $msg = [System.Web.HttpUtility]::HtmlDecode($msg)
                    }
                }

                # Pass message on in the pipeline
                $msg
            }

            # Display notices for this section
            if (($notices | Measure-Object).Count -gt 0)
            {
                "<u>Notices:</u><br><ul>"
                $notices | ForEach-Object {
                    ("<li>{0}</li>" -f $_)
                }
                "</ul><br>"
            }

            # Display output
            "<u>Content:</u><br>"
            $output | Out-String

            "<p />"
        } | Out-String

        $allSectionContent.Add($sectionContent) | Out-Null
    }

    end
    {
        # Display all notices here
        "<u>All Notices:</u><br><ul>"
        $allNotices.Keys | ForEach-Object {
            $key = $_
            ("<li>{0}</li>" -f $key)

            "<ul>"
            $allNotices[$key] | ForEach-Object {
                ("<li>{0}</li>" -f $_)
            }
            "</ul>"
        }
        "</ul><br>"

        # Display all section content
        $allSectionContent | ForEach-Object { $_ }

        # Wrap up HTML
        "</body></html>"
    }
}