LeetABit.Build.Help.psm1

#requires -version 6
using namespace System.Collections

Set-StrictMode -Version 3.0
Import-LocalizedData -BindingVariable LocalizedData -FileName LeetABit.Build.Help.Resources.psd1

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    if (Get-Module 'LeetABit.Build.Extensibility') {
        LeetABit.Build.Extensibility\Unregister-BuildExtension "LeetABit.Build.Help" -ErrorAction SilentlyContinue
    }
}

$Regex_ScriptBlockSyntax_FunctionName = '(?<={0})(.+?)(?=\[(-WhatIf|-Confirm|\<CommonParameters\>))'


##################################################################################################################
# Target Handlers
##################################################################################################################

Register-BuildTask "help" -Jobs {
    <#
    .SYNOPSIS
        Gets help for the build script or one of its targets.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Optional name of the build extension for which help shall be obtained.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $ExtensionTopic,

        # Optional name of the build task for which help shall be obtained.
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $TaskTopic
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }

    process {
        Get-BuildHelp $ExtensionTopic $TaskTopic | Out-Default
    }
}


##################################################################################################################
# Public Commands
##################################################################################################################


function Get-BuildHelp {
    <#
    .SYNOPSIS
        Gets help about build scripts usage.
    .DESCRIPTION
        Get-BuildHelp cmdlet provides a concise documentation about each of the loaded extensions and build tasks.
    .EXAMPLE
        PC> Get-BuildHelp
 
        Gets help about all registered build extensions and tasks.
    .EXAMPLE
        PC> Get-BuildHelp -ExtensionTopic "PowerShell"
 
        Gets a detailed help about all tasks provided by "PowerShell" extension.
    .EXAMPLE
        PC> Get-BuildHelp -TaskTopic "build"
 
        Gets a detailed help about all build commands provided by different extensions.
    .EXAMPLE
        PC> Get-BuildHelp -ExtensionTopic "PowerShell" -TaskTopic "build"
 
        Gets a detailed help about "build" task provided by "PowerShell" extension.
    #>

    [CmdletBinding(PositionalBinding = $False)]

    param (
        # Optional name of the build extension for which help shall be obtained.
        [Parameter(Position = 0,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $ExtensionTopic,

        # Optional name of the build task for which help shall be obtained.
        [Parameter(Position = 1,
                   Mandatory = $False,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $TaskTopic
    )

    begin {
        LeetABit.Build.Common\Import-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $scriptName = Join-Path '.' 'run.ps1'
    }

    process {
        $typeNameSuffix = if ($ExtensionTopic) {
            if ($TaskTopic) { 'DetailedView' } else { 'ExtensionView' }
        }
        else {
            if ($TaskTopic) { 'TaskView' } else { 'GeneralView' }
        }

        $helpInfo = @{}
        $helpInfo.ScriptName = $scriptName
        $helpInfo.Synopsis = $LocalizedData.Get_BuildHelp_Buildstrapper_Synopsis
        $helpInfo.TaskTopic = $TaskTopic
        $helpInfo.ExtensionTopic = $ExtensionTopic
        $helpInfo.Extensions = @()

        foreach ($currentExtension in LeetABit.Build.Extensibility\Get-BuildExtension) {
            if ($ExtensionTopic -and $ExtensionTopic -ne $currentExtension.Name) {
                continue
            }

            $extension = @{}
            $extension.Name = $currentExtension.Name
            $extension.Description = ''

            if ($currentExtension.Resolver.Module -and $currentExtension.Resolver.Module.Name -ne 'LeetABit.Build.Extensibility') {
                $extension.Description = $currentExtension.Resolver.Module.Description
            }

            $extension.Tasks = @{}

            foreach ($currentTask in $currentExtension.Tasks.Values) {
                if ($TaskTopic -and $TaskTopic -ne $currentTask.Name) {
                    continue
                }

                $task = @{}
                $task.Name = $currentTask.Name
                $task.IsDefault = $currentTask.IsDefault
                $task.Jobs = @()
                $task.Description = @()
                $task.Parameters = @()

                $currentTask.Jobs | ForEach-Object {
                    if ($_ -is [String]) {
                        $task.Jobs += $_
                        $task.Description += $extension.Tasks[$_].Description
                        $task.Parameters += $extension.Tasks[$_].Parameters
                    }
                    else {
                        if (-not $extension.Description) {
                            if ($_.Module) {
                                $extension.Description = $_.Module.Description
                            }
                        }

                        $jobScriptBlock = [String]$_
                        $function:private:ScriptBlockCommand = $jobScriptBlock
                        $helpObject = Get-Help 'ScriptBlockCommand'
                        $helpString = (Get-Help 'ScriptBlockCommand' -Full | Out-String)

                        $job = @{}
                        $job.Description = $helpObject.Synopsis
                        $task.Description += $helpObject.Synopsis

                        $nextSection = if ($helpObject.PSObject.TypeNames -contains 'ExtendedCmdletHelpInfo') {
                            'PARAMETERS'
                        }
                        else {
                            'DESCRIPTION'
                        }

                        $startIndex = $helpString.IndexOf("SYNTAX") + "SYNTAX".Length
                        $endIndex   = $helpString.IndexOf($nextSection, $startIndex)
                        $syntaxText = $helpString.Substring($startIndex, $endIndex - $startIndex).Trim()

                        $syntaxString = ($syntaxText -split [Environment]::NewLine) -join ''
                        $syntax = "$scriptName $($task.Name)"
                        $regex = $Regex_ScriptBlockSyntax_FunctionName -f 'ScriptBlockCommand'

                        if ($syntaxString -match $regex) {
                            $syntax += "$($matches[1])"
                        }

                        $job.Syntax = $syntax.Trim()
                        $job.Parameters = @()

                        if ($helpObject.parameters.PSObject.Properties.Name -contains 'parameter') {
                            foreach ($parameterObject in $helpObject.parameters.parameter) {
                                if (Get-Member -InputObject $parameterObject -Name "description" -ErrorAction SilentlyContinue) {
                                    $parameter = @{}
                                    $parameter.Name = $parameterObject.Name
                                    $parameter.Type = $parameterObject.type.name
                                    $parameter.Description = $parameterObject.description.Text
                                    $parameter.Mandatory = [System.Convert]::ToBoolean($parameterObject.required)
                                    $parameterObject = Convert-DictionaryToHelpObject $parameter 'Parameter' $typeNameSuffix
                                    $task.Parameters += $parameterObject
                                    $job.Parameters += $parameterObject
                                }
                            }
                        }

                        $task.Jobs += Convert-DictionaryToHelpObject $job 'Job' $typeNameSuffix
                    }
                }

                $parametersDictionary = @{}

                $task.Parameters | ForEach-Object {
                    if ($parametersDictionary.ContainsKey($_.Name)) {
                        $alreadyStored = $parametersDictionary[$_.Name]
                        $parametersDictionary.Remove($_.Name)

                        if ($alreadyStored.Type -ne $_.Type) {
                            $alreadyStored.Type = "String"
                        }

                        if ($alreadyStored.Description -notcontains $_.Description) {
                            $alreadyStored.Description += $_.Description
                        }

                        $alreadyStored.Mandatory = $alreadyStored.Mandatory -or $_.Mandatory
                        $parametersDictionary.Add($alreadyStored.Name, $alreadyStored)
                    }
                    else {
                        $_.Description = @($_.Description)
                        $parametersDictionary.Add($_.Name, $_)
                    }
                }

                $task.Description = ($task.Description | Get-Unique) -join " "
                $task.Parameters = $parametersDictionary.Values | Sort-Object -Property Name

                $extension.Tasks.Add($task.Name, (Convert-DictionaryToHelpObject $task 'Task' $typeNameSuffix))
            }

            if (-not $TaskTopic -or $extension.Tasks) {
                $extension.Tasks = $extension.Tasks.Values;
                $helpInfo.Extensions += Convert-DictionaryToHelpObject $extension 'Extension' $typeNameSuffix
            }
        }

        Convert-DictionaryToHelpObject $helpInfo 'HelpInfo' $typeNameSuffix
    }
}


##################################################################################################################
# Private Commands
##################################################################################################################



function Convert-DictionaryToHelpObject {
    <#
    .SYNOPSIS
        Converts a hashtable to a PSObject using keys as property names with associated values.
    #>

    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([String])]

    param (
        # A hashtable with desired object's properties.
        [Parameter(Position = 0,
                   Mandatory = $True,
                   ValueFromPipeline = $True,
                   ValueFromPipelineByPropertyName = $True)]
        [IDictionary]
        $Properties,

        # A name of the help object's type that shall be assigned to the object.
        [Parameter(Position = 1,
                   Mandatory = $True,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $HelpObjectName,

        # A name of the help object's type suffix that shall be assigned to the object as a secondary type.
        [Parameter(Position = 2,
                   Mandatory = $True,
                   ValueFromPipeline = $False,
                   ValueFromPipelineByPropertyName = $True)]
        [String]
        $HelpView
    )

    begin {
        $typeNameNamespace = 'LeetABit.Build.'
    }

    process {
        LeetABit.Build.Common\New-PSObject (($typeNameNamespace + $HelpObjectName + ".$HelpView"), ($typeNameNamespace + $HelpObjectName)) $Properties
    }
}


Export-ModuleMember -Function '*' -Variable '*' -Alias '*' -Cmdlet '*'