Checklist.psm1


enum Status {
    NotRun
    Pass
    Fail
}



class Log {

    # We originally used [Status] values as keys instead of casting [Status] values to strings.
    # We cast values to strings for now due to a bug in PSScriptAnalyzer v1.16.1
    static [hashtable] $Symbols = @{
        Pass   = [char]8730
        Fail   = "X"
        NotRun = " "
    }
    static [hashtable] $Colors = @{
        Pass   = "Green"
        NotRun = "Yellow"
        Fail   = "Red"
    }
    static [int] $LastLineLength

    static [void] WriteLine([string] $message, [Status] $status) {
        $message = [Log]::FormatMessage($message, $status)
        [Log]::LastLineLength = $message.Length
        $color = [Log]::Colors[ [string]$status ]
        Write-Host $message -ForegroundColor $color -NoNewline
    }

    static [void] OverwriteLine([string] $message, [Status] $status) {
        Write-Host "`r$(' ' * [Log]::LastLineLength)" -NoNewline
        $message = [Log]::FormatMessage($message, $status)
        [Log]::LastLineLength = $message.Length
        $color = [Log]::Colors[ [string]$status ]
        Write-Host "`r$message" -ForegroundColor $color
    }

    static [string] FormatMessage([string] $message, [Status] $status) {
        $symbol = [Log]::Symbols[ [string]$status ]
        return "$(Get-Date -Format 'hh:mm:ss') [ $symbol ] $message"
    }

    static [void] WriteError([string] $message) {
        Write-Host "`n$message`n" -ForegroundColor Red
        exit -1
    }
}





<#
.SYNOPSIS
Ensures a requirement is met.
 
.DESCRIPTION
This cmdlet allows for declaratively defining requirements and implementing consistent logging and idempotency around the status of the requirements.
 
.PARAMETER Describe
A description of the requirement that is enforced.
 
.PARAMETER Test
If present, 'Test' is a scriptblock that returns 'true' if the requirement is already met and the 'Set' scriptblock should not run. If not present, 'Set' will always run.
 
.PARAMETER Set
A scriptblock that imposes the requirement when run. If a "Test' scriptblock is not provided, 'Set' must be idempotent.
 
.PARAMETER Message
An error message printed if an idempotent 'Set' scriptblock fails during execution.
 
.EXAMPLE
# A non-idempotent 'Set' scriptblock
Invoke-ChecklistRequirement `
    -Describe "'Hello world' is logged" `
    -Test {Get-Content $MyLogFilePath | ? {$_ -eq "Hello world"}} `
    -Set {"Hello world" >> $MyLogFilePath}
 
# An idempotent 'Set' scriptblock
Invoke-ChecklistRequirement `
    -Describe "'Hello world' is logged" `
    -Set {"Hello world" > $MyLogFilePath} `
    -Message "Could not log 'Hello World'"
#>

function Invoke-ChecklistRequirement {
    Param(
        [Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
        [Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
        [Parameter(Mandatory, ParameterSetName = "Information")]
        [ValidateNotNullOrEmpty()]
        [string] $Describe,
        [Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
        [ValidateNotNullOrEmpty()]
        [scriptblock] $Test,
        [Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
        [Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
        [ValidateNotNullOrEmpty()]
        [scriptblock] $Set,
        [Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
        [ValidateNotNullOrEmpty()]
        [string] $Message,
        [switch] $ListRequirement
    )


    try {

        if ($ListRequirement) {
            [Log]::WriteLine("$Describe`n", [Status]::NotRun)
            return
        }

        switch ($PSCmdlet.ParameterSetName) {

            "ApplyIfNeeded" {
                [Log]::WriteLine($Describe, [Status]::NotRun)
                if (&$Test) {
                    [Log]::OverwriteLine($Describe, [Status]::Pass)
                }
                else {
                    &$Set | Out-Null
                    if (&$Test) {
                        [Log]::OverwriteLine($Describe, [Status]::Pass)
                    }
                    else {
                        [Log]::OverwriteLine($Describe, [Status]::Fail)
                        [Log]::WriteError("Requirement validation failed")
                    }
                }
            }

            "ApplyAlways" {
                [Log]::WriteLine($Describe, [Status]::NotRun)
                if (&$Set) {
                    [Log]::OverwriteLine($Describe, [Status]::Pass)
                }
                else {
                    [Log]::OverwriteLine($Describe, [Status]::Fail)
                    [Log]::WriteError($Message)
                }
            }

            "Information" {
                [Log]::WriteLine("$Describe`n", [Status]::NotRun)
            }

        }

    }
    catch {
        [Log]::OverwriteLine($Describe, [Status]::Fail)
        Write-Host ""
        throw $_
    }
}



function Invoke-ChecklistDscRequirement {
    Param(
        [string]$Describe,
        [string]$ResourceName,
        [string]$ModuleName,
        [hashtable]$Property
    )

    $dscParams = @{
        Name       = $ResourceName
        ModuleName = $ModuleName
        Property   = $Property
    }

    Invoke-ChecklistRequirement `
        -Describe $Describe `
        -Test {Invoke-DscResource -Method "Test" @dscParams} `
        -Set {Invoke-DscResource -Method "Set" @dscParams}

}