Private/Classes.ps1

<#
.DESCRIPTION
    Private classes for Brownserve.PSBuildTools.
    All classes are consolidated here to guarantee load order and avoid 'type not found' errors
    when classes depend on other classes defined in this module.
#>


## Changelog and versioning classes

<#
    Simple class to ensure datetime objects are displayed as short dates in output but retain their date time attribute
#>

class BrownserveShortDate
{
    [datetime]$Date

    BrownserveShortDate([datetime]$Date)
    {
        $this.Date = $Date
    }

    BrownserveShortDate([string]$Date)
    {
        $this.Date = $Date
    }

    [string] ToString()
    {
        return "$(Get-Date $this.Date -Format 'yyyy/MM/dd')"
    }
}

<#
    This class helps us to format version history entries from a changelog
#>

class BrownserveVersionHistory
{
    [semver]$Version
    [BrownserveShortDate]$ReleaseDate
    [string]$URL
    [string[]]$ReleaseNotes
    [bool]$PreRelease = $false

    BrownserveVersionHistory([semver]$Version, [datetime]$ReleaseDate, [string]$URL, [string]$ReleaseNotes)
    {
        $this.Version = $Version
        $this.ReleaseDate = $ReleaseDate
        $this.URL = $URL
        $this.ReleaseNotes = $ReleaseNotes
        if ($this.Version.PreReleaseLabel)
        {
            $this.PreRelease = $true
        }
    }

    BrownserveVersionHistory([pscustomobject]$VersionHistory)
    {
        if (!$VersionHistory.Version)
        {
            throw 'Cannot create BrownserveVersionHistory object without a Version'
        }
        if (!$VersionHistory.ReleaseDate)
        {
            throw 'Cannot create BrownserveVersionHistory object without a ReleaseDate'
        }
        if (!$VersionHistory.URL)
        {
            throw 'Cannot create BrownserveVersionHistory object without a URL'
        }
        if (!$VersionHistory.ReleaseNotes)
        {
            throw 'Cannot create BrownserveVersionHistory object without ReleaseNotes'
        }
        $this.Version = $VersionHistory.Version
        $this.ReleaseDate = $VersionHistory.ReleaseDate
        $this.URL = $VersionHistory.URL
        $this.ReleaseNotes = $VersionHistory.ReleaseNotes
        if ($this.Version.PreReleaseLabel)
        {
            $this.PreRelease = $true
        }
    }

    BrownserveVersionHistory([hashtable]$VersionHistory)
    {
        if (!$VersionHistory.Version)
        {
            throw 'Cannot create BrownserveVersionHistory object without a Version'
        }
        if (!$VersionHistory.ReleaseDate)
        {
            throw 'Cannot create BrownserveVersionHistory object without a ReleaseDate'
        }
        if (!$VersionHistory.URL)
        {
            throw 'Cannot create BrownserveVersionHistory object without a URL'
        }
        if (!$VersionHistory.ReleaseNotes)
        {
            throw 'Cannot create BrownserveVersionHistory object without ReleaseNotes'
        }
        $this.Version = $VersionHistory.Version
        $this.ReleaseDate = $VersionHistory.ReleaseDate
        $this.URL = $VersionHistory.URL
        $this.ReleaseNotes = $VersionHistory.ReleaseNotes
        if ($this.Version.PreReleaseLabel)
        {
            $this.PreRelease = $true
        }
    }

    [string] ToString()
    {
        return "$($this.Version) - $($this.ReleaseDate)"
    }
}

<#
    Class for storing Brownserve Changelog data
#>

class BrownserveChangelog
{
    [BrownserveVersionHistory[]]$VersionHistory
    [int]$NewEntryInsertLine
    [BrownserveVersionHistory]$LatestVersion
    hidden [string]$ChangelogPath
    hidden [string[]]$Content
    [bool]$HasPlaceholder

    BrownserveChangelog([BrownserveVersionHistory[]]$VersionHistory, [int]$NewEntryInsertLine, [string]$ChangelogPath, [string[]]$Content)
    {
        $this.VersionHistory = $VersionHistory | Sort-Object -Property ReleaseDate -Descending
        $this.NewEntryInsertLine = $NewEntryInsertLine
        $this.LatestVersion = $this.VersionHistory[0]
        $this.ChangelogPath = $ChangelogPath
        $this.Content = $Content
    }

    BrownserveChangelog([pscustomobject]$Changelog)
    {
        if (!$Changelog.VersionHistory)
        {
            throw 'Cannot create BrownserveChangelog object without VersionHistory'
        }
        if (!$Changelog.NewEntryInsertLine)
        {
            throw 'Cannot create BrownserveChangelog object without NewEntryInsertLine'
        }
        if (!$Changelog.ChangelogPath)
        {
            throw 'Cannot create BrownserveChangelog object without ChangelogPath'
        }
        if (!$Changelog.Content)
        {
            throw 'Cannot create BrownserveChangelog object without Content'
        }
        $this.VersionHistory = $Changelog.VersionHistory | Sort-Object -Property ReleaseDate -Descending
        $this.NewEntryInsertLine = $Changelog.NewEntryInsertLine
        $this.LatestVersion = $this.VersionHistory[0]
        $this.ChangelogPath = $Changelog.ChangelogPath
        $this.Content = $Changelog.Content
        $this.HasPlaceholder = [bool]$Changelog.HasPlaceholder
    }

    BrownserveChangelog([hashtable]$Changelog)
    {
        if (!$Changelog.VersionHistory)
        {
            throw 'Cannot create BrownserveChangelog object without VersionHistory'
        }
        if (!$Changelog.NewEntryInsertLine)
        {
            throw 'Cannot create BrownserveChangelog object without NewEntryInsertLine'
        }
        if (!$Changelog.ChangelogPath)
        {
            throw 'Cannot create BrownserveChangelog object without ChangelogPath'
        }
        if (!$Changelog.Content)
        {
            throw 'Cannot create BrownserveChangelog object without Content'
        }
        $this.VersionHistory = $Changelog.VersionHistory | Sort-Object -Property ReleaseDate -Descending
        $this.NewEntryInsertLine = $Changelog.NewEntryInsertLine
        $this.LatestVersion = $this.VersionHistory[0]
        $this.ChangelogPath = $Changelog.ChangelogPath
        $this.Content = $Changelog.Content
        $this.HasPlaceholder = [bool]$Changelog.HasPlaceholder
    }
}

## IDE / EditorConfig classes

<#
    This class helps us format editorconfig properties
#>

class EditorConfigProperty
{
    [string]$Name
    $Value

    EditorConfigProperty([string]$Name, $Value)
    {
        $this.Name = $Name
        $this.Value = $Value
        $this.ValidityCheck()
    }

    EditorConfigProperty([hashtable]$Property)
    {
        $this.Value = $Property.Value
        $this.Name = $Property.Name
        $this.ValidityCheck()
    }

    EditorConfigProperty([System.Collections.DictionaryEntry]$Property)
    {
        $this.Value = $Property.Value
        $this.Name = $Property.Name
        $this.ValidityCheck()
    }

    [string] ToString()
    {
        return ("$($this.Name) = $($this.Value)").ToLower()
    }

    hidden ValidityCheck()
    {
        $ValidPropertyNames = @(
            'indent_style',
            'indent_size',
            'tab_width',
            'end_of_line',
            'charset',
            'trim_trailing_whitespace',
            'insert_final_newline',
            'max_line_length'
        )
        if ($this.Name -notin $ValidPropertyNames)
        {
            throw "Invalid editorconfig property name: '$($this.Name)'"
        }

        switch ($this.Name)
        {
            'indent_style'
            {
                $ValidValues = @('tab', 'space')
                if ($this.Value -notin $ValidValues)
                {
                    throw "Invalid indent_style value: '$($this.Value)'"
                }
            }
            'indent_size'
            {
                (($this.Value -isnot [int]) -or ($this.Value -isnot [Int64]))
                {
                    if ($this.Value -ne 'tab')
                    {
                        throw "Invalid indent_size value: '$($this.Value)'"
                    }
                }
            }
            'tab_width'
            {
                (($this.Value -isnot [int]) -or ($this.Value -isnot [Int64]))
                {
                    throw "Invalid tab_width value: '$($this.Value)'"
                }
            }
            'end_of_line'
            {
                $ValidValues = @('lf', 'cr', 'crlf')
                if ($this.Value -notin $ValidValues)
                {
                    throw "Invalid end_of_line value: '$($this.Value)'"
                }
            }
            'charset'
            {
                $ValidValues = @('latin1', 'utf-8', 'utf-8-bom', 'utf-16be', 'utf-16le')
                if ($this.Value -notin $ValidValues)
                {
                    throw "Invalid charset value: '$($this.Value)'"
                }
            }
            'trim_trailing_whitespace'
            {
                if ($this.Value -isnot [bool])
                {
                    throw "Invalid trim_trailing_whitespace value: '$($this.Value)'"
                }
            }
            'insert_final_newline'
            {
                if ($this.Value -isnot [bool])
                {
                    throw "Invalid insert_final_newline value: '$($this.Value)'"
                }
            }
            'max_line_length'
            {
                if (($this.Value -isnot [int]) -or ($this.Value -isnot [Int64]))
                {
                    if ($this.Value -ne 'off')
                    {
                        throw "Invalid max_line_length value: '$($this.Value)'"
                    }
                }
            }
        }
    }
}

<#
    This class helps us format editorconfig sections
#>

class EditorConfigSection
{
    [string]$FilePath
    [EditorConfigProperty[]]$Properties
    [string[]]$Comment

    EditorConfigSection([string]$FilePath, [EditorConfigProperty[]]$Properties)
    {
        $this.FilePath = $FilePath
        $this.Properties = $Properties
    }

    EditorConfigSection([string]$FilePath, [EditorConfigProperty[]]$Properties, [string[]]$Comment)
    {
        $this.FilePath = $FilePath
        $this.Properties = $Properties
        $this.Comment = $Comment
    }

    EditorConfigSection([hashtable]$Section)
    {
        if (!$Section.FilePath)
        {
            throw 'Cannot create EditorConfigSection object without FilePath'
        }
        if (!$Section.Properties)
        {
            throw 'Cannot create EditorConfigSection object without Properties'
        }
        if ($Section.Comment)
        {
            $this.Comment = $Section.Comment
        }
        $this.FilePath = $Section.FilePath
        if ($Section.Properties -is [hashtable])
        {
            $this.ExpandProperties($Section.Properties)
        }
        else
        {
            $this.Properties = $Section.Properties
        }
    }

    hidden ExpandProperties([hashtable]$Properties)
    {
        $ExpandedProps = @()
        $Properties.GetEnumerator() | ForEach-Object {
            $ExpandedProps += [EditorConfigProperty]$_
        }
        $this.Properties = $ExpandedProps
    }

    [string] ToString()
    {
        $Return = ''
        if ($this.Comment)
        {
            $this.Comment | ForEach-Object {
                if ($_.StartsWith('#'))
                {
                    $Return += "$_`n"
                }
                else
                {
                    $Return += "# $_`n"
                }
            }
        }
        if ($this.FilePath -notmatch '^\[.*\]$')
        {
            $Return += "[$($this.FilePath)]`n"
        }
        else
        {
            $Return += "$($this.FilePath)`n"
        }
        $this.Properties | ForEach-Object {
            $Return += "$($_)`n"
        }
        return $Return
    }
}

## Git / .gitignore classes

class GitIgnore
{
    [string[]]$Item
    [string]$Comment

    GitIgnore([hashtable]$Hashtable)
    {
        if (!$Hashtable.Item)
        {
            throw "Hashtable does not contain a key named 'Item'"
        }
        $this.Item = $Hashtable.Item
        if ($Hashtable.Comment)
        {
            # Try to ensure every line starts with the pound symbol
            $LocalComment = $Hashtable.Comment -split "`n"
            $SanitizedComment = ""
            $LocalComment | ForEach-Object {
                if ($_ -notmatch '^\#')
                {
                    $SanitizedComment += "# $_"
                }
                else
                {
                    $SanitizedComment += $_
                }
                if ($_ -notmatch $LocalComment[-1])
                {
                    $SanitizedComment += "`n"
                }
            }
            $this.Comment = $SanitizedComment
        }
    }

    GitIgnore([PSCustomObject]$Object)
    {
        if (!$Object.Item)
        {
            throw "Hashtable does not contain a key named 'Item'"
        }
        $this.Item = $Object.Item
        if ($Object.Comment)
        {
            # Try to ensure every line starts with the pound symbol
            $LocalComment = $Object.Comment -split "`n"
            $SanitizedComment = ""
            $LocalComment | ForEach-Object {
                if ($_ -notmatch '^\#')
                {
                    $SanitizedComment += "# $_"
                }
                else
                {
                    $SanitizedComment += $_
                }
                if ($_ -notmatch $LocalComment[-1])
                {
                    $SanitizedComment += "`n"
                }
            }
            $this.Comment = $SanitizedComment
        }
    }

    GitIgnore([string]$Item)
    {
        $this.Item = $Item
    }

    GitIgnore([string]$Item, [string]$Comment)
    {
        $this.Item = $Item
        # Try to ensure every line starts with the pound symbol
        $LocalComment = $Comment -split "`n"
        $SanitizedComment = ""
        $LocalComment | ForEach-Object {
            if ($_ -notmatch '^\#')
            {
                $SanitizedComment += "# $_"
            }
            else
            {
                $SanitizedComment += $_
            }
            if ($_ -notmatch $LocalComment[-1])
            {
                $SanitizedComment += "`n"
            }
        }
        $this.Comment = $SanitizedComment
    }
}

## Build infrastructure classes

class InitPath
{
    [string] $VariableName
    [string] $Path
    [array] $ChildPaths
    [string] $Description
    [string] $PathType

    InitPath([pscustomobject]$InitPath)
    {
        $RequiredProps = @('path','VariableName','PathType')
        foreach ($Prop in $RequiredProps)
        {
            if (!$InitPath.$Prop)
            {
                throw "Object missing property '$Prop'"
            }
            else
            {
                $this.$Prop = $InitPath.$Prop
            }
        }
        if ($InitPath.ChildPaths)
        {
            $this.ChildPaths = $InitPath.ChildPaths
        }
        if ($InitPath.Description)
        {
            $this.Description = $InitPath.Description
        }
    }

    InitPath([hashtable]$InitPath)
    {
        $RequiredKeys = @('path','VariableName','PathType')
        foreach ($Key in $RequiredKeys)
        {
            if (!$InitPath.$Key)
            {
                throw "Hashtable missing property '$Key'"
            }
            else
            {
                $this.$Key = $InitPath.$Key
            }
        }
        if ($InitPath.ChildPaths)
        {
            $this.ChildPaths = $InitPath.ChildPaths
        }
        if ($InitPath.Description)
        {
            $this.Description = $InitPath.Description
        }
    }
}

enum BrownserveCICD
{
    GitHubActions
    TeamCity
}

enum BrownserveRepoProjectType
{
    PowerShellModule
    BrownservePSTools
    Generic
}

class GitHubActionsJob
{
    [string]$JobTitle
    [string]$RunsOn
    [hashtable[]]$Steps

    GitHubActionsJob([hashtable]$Hash)
    {
        $RequiredKeys = @('JobTitle', 'RunsOn', 'Steps')
        foreach ($Key in $RequiredKeys)
        {
            if (!$Hash.$Key)
            {
                throw "Hashtable missing key '$Key'"
            }
            else
            {
                $this.$Key = $Hash.$Key
            }
        }
    }
}

class PaketDependencyRule
{
    [string]$Source
    [string]$PackageName

    PaketDependencyRule([hashtable]$Hashtable)
    {
        $RequiredKeys = @('Source', 'PackageName')
        foreach ($Key in $RequiredKeys)
        {
            if (!$Hashtable.$Key)
            {
                throw "Hashtable missing key '$Key'"
            }
            else
            {
                $this.$Key = $Hashtable.$Key
            }
        }
    }

    PaketDependencyRule([pscustomobject]$Object)
    {
        $RequiredKeys = @('Source', 'PackageName')
        foreach ($Key in $RequiredKeys)
        {
            if (!$Object.$Key)
            {
                throw "Object missing property '$Key'"
            }
            else
            {
                $this.$Key = $Object.$Key
            }
        }
    }
}

class PaketDependency
{
    [PaketDependencyRule[]]$Rule
    [string]$Comment

    PaketDependency([hashtable]$Hashtable)
    {
        if (!$Hashtable.Rule)
        {
            throw "Hashtable missing key 'Rule'"
        }

        $this.Rule = $Hashtable.Rule
        if ($Hashtable.Comment)
        {
            $LocalComment = $Hashtable.Comment -split "`n"
            $SanitizedComment = ''
            $LocalComment | ForEach-Object {
                if ($_ -notmatch '^\#')
                {
                    $SanitizedComment += "# $_"
                }
                else
                {
                    $SanitizedComment += $_
                }
                if ($_ -notmatch $LocalComment[-1])
                {
                    $SanitizedComment += "`n"
                }
            }
            $this.Comment = $SanitizedComment
        }
    }

    PaketDependency([pscustomobject]$Object)
    {
        if (!$Object.Rule)
        {
            throw "Hashtable missing key 'Rule'"
        }

        $this.Rule = $Object.Rule
        if ($Object.Comment)
        {
            $LocalComment = $Object.Comment -split "`n"
            $SanitizedComment = ''
            $LocalComment | ForEach-Object {
                if ($_ -notmatch '^\#')
                {
                    $SanitizedComment += "# $_"
                }
                else
                {
                    $SanitizedComment += $_
                }
                if ($_ -notmatch $LocalComment[-1])
                {
                    $SanitizedComment += "`n"
                }
            }
            $this.Comment = $SanitizedComment
        }
    }
}

class PackageAlias
{
    [string] $Alias
    [string] $FileName
    [string] $VariableName

    PackageAlias([hashtable]$Hash)
    {
        $RequiredKeys = @('Alias','FileName')
        foreach ($Key in $RequiredKeys)
        {
            if (!$Hash.$Key)
            {
                throw "Hashtable missing key '$Key'"
            }
            else
            {
                $this.$Key = $Hash.$Key
            }
        }
        if ($Hash.VariableName)
        {
            $this.VariableName = $Hash.VariableName
        }
    }

    PackageAlias([pscustomobject]$Obj)
    {
        $RequiredProps = @('Alias','FileName')
        foreach ($Prop in $RequiredProps)
        {
            if (!$Obj.$Prop)
            {
                throw "Object missing property '$Prop'"
            }
            else
            {
                $this.$Prop = $Obj.$Prop
            }
        }
        if ($Obj.VariableName)
        {
            $this.VariableName = $Obj.VariableName
        }
    }
}

## VSCode classes

class BrownserveVSCodeExtension
{
    [string]$ExtensionID
    [hashtable]$Settings

    BrownserveVSCodeExtension([hashtable]$Hash)
    {
        $this.ExtensionID = $Hash.ExtensionID
        $this.Settings = $Hash.Settings
    }

    BrownserveVSCodeExtension([string]$ExtensionID, [hashtable]$Hash)
    {
        $this.ExtensionID = $ExtensionID
        $this.Settings = $Hash
    }
}

## PowerShell module classes

class BrownservePowerShellModule
{
    [string]$Name
    [string]$Description
    [guid]$GUID
    [string[]]$Tags
    [string[]]$RequiredModules

    BrownservePowerShellModule([hashtable]$Hashtable)
    {
        $RequiredKeys = @('name','description','guid','tags')
        foreach ($Key in $RequiredKeys)
        {
            $MatchingKey = $Hashtable.Keys | Where-Object { $_ -ieq $Key } | Select-Object -First 1
            if (!$MatchingKey)
            {
                throw "Hashtable missing required key '$Key'"
            }
            $this.$Key = $Hashtable[$MatchingKey]
        }
        $RequiredModulesKey = $Hashtable.Keys | Where-Object { $_ -ieq 'RequiredModules' } | Select-Object -First 1
        if ($RequiredModulesKey)
        {
            $this.RequiredModules = $Hashtable[$RequiredModulesKey]
        }
    }
}