PSSemVer.psm1

[CmdletBinding()]
param()

$scriptName = 'PSSemVer'
Write-Verbose "[$scriptName] - Importing module"

#region - From [classes] - [public]
Write-Verbose "[$scriptName] - [classes] - [public] - Processing folder"

#region - From [classes] - [public] - [PSSemVer]
Write-Verbose "[$scriptName] - [classes] - [public] - [PSSemVer] - Importing"

class PSSemVer : System.Object, System.IComparable, System.IEquatable[Object] {
    #region Static properties
    hidden static [string] $PSSemVerPattern = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' +
    '(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
    hidden static [string] $LoosePSSemVerPattern = '^(?:([a-zA-Z]*)-?)?(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?' +
    '(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
    #endregion Static properties

    #region Properties
    [string] $Prefix
    [int] $Major
    [int] $Minor
    [int] $Patch
    [string] $Prerelease
    [string] $BuildMetadata
    #endregion Properties

    #region Constructors
    PSSemVer() {}

    PSSemVer([int]$Major) {
        $this.Major = $Major
    }

    PSSemVer([int]$Major, [int]$Minor) {
        $this.Major = $Major
        $this.Minor = $Minor
    }

    PSSemVer([int]$Major, [int]$Minor, [int]$Patch) {
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
    }

    PSSemVer([int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel) {
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
        $this.Prerelease = $PreReleaseLabel
    }

    PSSemVer([int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel, [string]$BuildLabel) {
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
        $this.Prerelease = [string]$PreReleaseLabel
        $this.BuildMetadata = $BuildLabel
    }

    PSSemVer([string]$Prefix, [int]$Major) {
        $this.Prefix = $Prefix
        $this.Major = $Major
    }

    PSSemVer([string]$Prefix, [int]$Major, [int]$Minor) {
        $this.Prefix = $Prefix
        $this.Major = $Major
        $this.Minor = $Minor
    }

    PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch) {
        $this.Prefix = $Prefix
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
    }

    PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel) {
        $this.Prefix = $Prefix
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
        $this.Prerelease = $PreReleaseLabel
    }

    PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel, [string]$BuildLabel) {
        $this.Prefix = $Prefix
        $this.Major = $Major
        $this.Minor = $Minor
        $this.Patch = $Patch
        $this.Prerelease = $PreReleaseLabel
        $this.BuildMetadata = $BuildLabel
    }

    PSSemVer([string]$version) {
        if ($version -match [PSSemVer]::PSSemVerPattern) {
            Write-Debug 'PSSemVerPattern'
            $this.Major = [int]$Matches[1]
            $this.Minor = [int]$Matches[2]
            $this.Patch = [int]$Matches[3]
            $this.Prerelease = $Matches[4]
            $this.BuildMetadata = $Matches[5]
        } elseif ($version -match [PSSemVer]::LoosePSSemVerPattern) {
            Write-Debug 'LoosePSSemVerPattern'
            $this.Prefix = $Matches[1]
            $this.Major = [int]$Matches[2]
            $this.Minor = [int]$Matches[3]
            $this.Patch = [int]$Matches[4]
            $this.Prerelease = $Matches[5]
            $this.BuildMetadata = $Matches[6]
        } else {
            throw [ArgumentException]::new('The version string is not a valid SemVer string')
        }
    }

    PSSemVer([version]$version) {
        $this.Major = if ($version.Major -lt 0) { 0 } else { $version.Major }
        $this.Minor = if ($version.Minor -lt 0) { 0 } else { $version.Minor }
        $this.Patch = if ($version.Build -lt 0) { 0 } else { $version.Build }
    }

    PSSemVer([string]$version, [string]$PreReleaseLabel, [string]$BuildLabel) {
        if ($version -match [PSSemVer]::PSSemVerPattern) {
            $this.Major = [int]$Matches[1]
            $this.Minor = [int]$Matches[2]
            $this.Patch = [int]$Matches[3]
            $this.Prerelease = $PreReleaseLabel
            $this.BuildMetadata = $BuildLabel
        } else {
            throw [ArgumentException]::new('The version string is not a valid SemVer string')
        }
    }
    #endregion Constructors

    #region Methods
    [void] BumpMajor() {
        $this.Major = $this.Major + 1
        $this.Minor = 0
        $this.Patch = 0
    }

    [void] BumpMinor() {
        $this.Minor = $this.Minor + 1
        $this.Patch = 0
    }

    [void] BumpPatch() {
        $this.Patch = $this.Patch + 1
    }

    [void] BumpPrereleaseNumber() {
        if (-not [string]::IsNullOrEmpty($this.Prerelease)) {
            $PrereleaseNumber = [PSSemVer]::GetPrereleaseNumber($this.Prerelease)
            if ([string]::IsNullOrEmpty($PrereleaseNumber)) {
                $PrereleaseNumber = 1
            } else {
                $PrereleaseNumber++
            }
            $this.PreRelease = "$([PSSemVer]::GetPrereleaseLabel($this.Prerelease)).$PrereleaseNumber"
        }
    }

    [void] SetPrerelease([string]$label) {
        $this.Prerelease = $label
    }

    [void] SetPrereleaseLabel([string]$label) {
        $this.SetPrerelease($label)
    }

    [void] SetBuild([string]$label) {
        $this.SetBuildMetadata($label)
    }

    [void] SetBuildLabel([string]$label) {
        $this.SetBuildMetadata($label)
    }

    [void] SetBuildMetadata([string]$label) {
        $this.BuildMetadata = $label
    }

    [int] CompareTo([Object]$other) {
        if (-not $other -is [PSSemVer]) {
            throw [ArgumentException]::new('The argument must be of type PSSemVer')
        }
        if ($this.Major -lt $other.Major) {
            return -1
        }
        if ($this.Major -gt $other.Major) {
            return 1
        }
        if ($this.Minor -lt $other.Minor) {
            return -1
        }
        if ($this.Minor -gt $other.Minor) {
            return 1
        }
        if ($this.Patch -lt $other.Patch) {
            return -1
        }
        if ($this.Patch -gt $other.Patch) {
            return 1
        }
        if ([string]::IsNullOrEmpty($this.Prerelease) -and [string]::IsNullOrEmpty($other.Prerelease)) {
            return 0
        }
        if ([string]::IsNullOrEmpty($this.Prerelease)) {
            return 1
        }
        if ([string]::IsNullOrEmpty($other.Prerelease)) {
            return -1
        }
        $thisPrereleaseArray = ($this.Prerelease -split '\.')
        $otherPrereleaseArray = ($other.Prerelease -split '\.')
        for ($i = 0; $i -lt [Math]::Max($thisPrereleaseArray.Length, $otherPrereleaseArray.Length); $i++) {
            if ($i -ge $thisPrereleaseArray.Length) {
                return -1
            }
            if ($i -ge $otherPrereleaseArray.Length) {
                return 1
            }
            if ($thisPrereleaseArray[$i] -eq $otherPrereleaseArray[$i]) {
                continue
            }
            if ($thisPrereleaseArray[$i] -match '^\d+$' -and $otherPrereleaseArray[$i] -match '^\d+$') {
                return [int]$thisPrereleaseArray[$i] - [int]$otherPrereleaseArray[$i]
            }
            if ($thisPrereleaseArray[$i] -match '^\d+$' -and $otherPrereleaseArray[$i] -notmatch '^\d+$') {
                return -1
            }
            if ($thisPrereleaseArray[$i] -notmatch '^\d+$' -and $otherPrereleaseArray[$i] -match '^\d+$') {
                return 1
            }
            return $thisPrereleaseArray[$i].CompareTo($otherPrereleaseArray[$i])
        }

        return 0
    }

    [bool] Equals([Object]$other) {
        if (-not $other -is [PSSemVer]) {
            return $false
        }
        if ($this.Major -ne $other.Major) {
            return $false
        }
        if ($this.Minor -ne $other.Minor) {
            return $false
        }
        if ($this.Patch -ne $other.Patch) {
            return $false
        }
        if ($this.Prerelease -ne $other.Prerelease) {
            return $false
        }
        if ($this.BuildMetadata -ne $other.BuildMetadata) {
            return $false
        }
        return $true
    }

    [string] ToString() {
        [string]$output = "$($this.Prefix)$($this.Major).$($this.Minor).$($this.Patch)"

        if (-not [string]::IsNullOrEmpty($this.Prerelease)) {
            $output += "-$($this.Prerelease)"
        }
        if (-not [string]::IsNullOrEmpty($this.BuildMetadata)) {
            $output += "+$($this.BuildMetadata)"
        }
        return $output
    }
    #endregion Methods

    #region Static Methods
    static [PSSemVer] Parse([string]$string) {
        return [PSSemVer]::new($string)
    }

    static [Nullable[int]] GetPrereleaseNumber([string]$string) {
        if ($string -match '^(.*?)(?:\.(\d+))?$') {
            return [Nullable[int]]$matches[2]
        }
        return $null
    }

    static [string] GetPrereleaseLabel([string]$string) {
        if ($string -match '^(.*?)(?:\.(\d+))?$') {
            return $matches[1]
        }
        return $null
    }
    #endregion Static Methods
}

Write-Verbose "[$scriptName] - [classes] - [public] - [PSSemVer] - Done"
#endregion - From [classes] - [public] - [PSSemVer]

Write-Verbose "[$scriptName] - [classes] - [public] - Done"
#endregion - From [classes] - [public]

#region - From [functions] - [public]
Write-Verbose "[$scriptName] - [functions] - [public] - Processing folder"

#region - From [functions] - [public] - [ConvertTo-PSSemVer]
Write-Verbose "[$scriptName] - [functions] - [public] - [ConvertTo-PSSemVer] - Importing"

filter ConvertTo-PSSemVer {
    <#
        .SYNOPSIS
        Converts a version string to a PSSemVer object.

        .DESCRIPTION
        This function takes a version string and converts it to a PSSemVer object.

        .EXAMPLE
        '1.2.3-alpha.1+001' | ConvertTo-SemVer

        Major : 1
        Minor : 2
        Patch : 3
        Prerelease : alpha.1
        BuildMetadata : 001

        .NOTES
        Compatible with SemVer 2.0.0.
    #>

    [OutputType([PSSemVer])]
    [CmdletBinding()]
    param (
        # The version to convert.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName)
        ]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $Version
    )

    if ([string]::IsNullOrEmpty($Version)) {
        return New-PSSemVer
    }

    try {
        $PSSemVer = [PSSemVer]::new($Version)
        return $PSSemVer
    } catch {
        throw "Failed to convert '$Version' to PSSemVer."
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ConvertTo-PSSemVer] - Done"
#endregion - From [functions] - [public] - [ConvertTo-PSSemVer]
#region - From [functions] - [public] - [New-PSSemVer]
Write-Verbose "[$scriptName] - [functions] - [public] - [New-PSSemVer] - Importing"

function New-PSSemVer {
    <#
        .SYNOPSIS
        Creates a new PSSemVer object.

        .DESCRIPTION
        This function creates a new PSSemVer object.

        .EXAMPLE
        New-SemVer -Version '1.2.3-alpha.1+001'

        Major : 1
        Minor : 2
        Patch : 3
        Prerelease : alpha.1
        BuildMetadata : 001

        .EXAMPLE
        New-SemVer -Major 1 -Minor 2 -Patch 3 -Prerelease 'alpha.1' -BuildMetadata '001'

        Major : 1
        Minor : 2
        Patch : 3
        Prerelease : alpha.1
        BuildMetadata : 001

        .NOTES
        Compatible with SemVer 2.0.0.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Does not change system state, but creates a new object.'
    )]
    [OutputType([PSSemVer])]
    [CmdletBinding(DefaultParameterSetName = 'Values')]
    param (
        # The major version.
        [Parameter(ParameterSetName = 'Values')]
        [int] $Major = 0,

        # The minor version.
        [Parameter(ParameterSetName = 'Values')]
        [int] $Minor = 0,

        # The patch version.
        [Parameter(ParameterSetName = 'Values')]
        [int] $Patch = 0,

        # The prerelease version.
        [Parameter(ParameterSetName = 'Values')]
        [Parameter(ParameterSetName = 'String')]
        [Alias('PreReleaseLabel')]
        [string] $Prerelease = '',

        # The build metadata.
        [Parameter(ParameterSetName = 'Values')]
        [Parameter(ParameterSetName = 'String')]
        [Alias('Build', 'BuildLabel')]
        [string] $BuildMetadata = '',

        # The version as a string.
        [Parameter(
            Mandatory,
            ParameterSetName = 'String'
        )]
        [AllowEmptyString()]
        [string] $Version
    )

    switch ($PSCmdlet.ParameterSetName) {
        'Values' {
            return [PSSemVer]::New($Major, $Minor, $Patch, $Prerelease, $BuildMetadata)
        }
        'String' {
            if ([string]::IsNullOrEmpty($Version)) {
                $Version = '0.0.0'
            }
            $obj = [PSSemVer]::New($Version)
            if ($BuildMetadata) {
                $obj.SetBuildMetadata($BuildMetadata)
            }
            if ($Prerelease) {
                $obj.SetPrerelease($Prerelease)
            }
            return $obj
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [New-PSSemVer] - Done"
#endregion - From [functions] - [public] - [New-PSSemVer]

Write-Verbose "[$scriptName] - [functions] - [public] - Done"
#endregion - From [functions] - [public]

# Get the internal TypeAccelerators class to use its static methods.
$TypeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
# Ensure none of the types would clobber an existing type accelerator.
# If a type accelerator with the same name exists, throw an exception.
$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get
# Define the types to export with type accelerators.
$ExportableEnums = @(
)
$ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." }
foreach ($Type in $ExportableEnums) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        Write-Warning "Enum already exists [$($Type.FullName)]. Skipping."
    } else {
        Write-Verbose "Importing enum '$Type'."
        $TypeAcceleratorsClass::Add($Type.FullName, $Type)
    }
}
$ExportableClasses = @(
    [PSSemVer]
)
$ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." }
foreach ($Type in $ExportableClasses) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        Write-Warning "Class already exists [$($Type.FullName)]. Skipping."
    } else {
        Write-Verbose "Importing class '$Type'."
        $TypeAcceleratorsClass::Add($Type.FullName, $Type)
    }
}

# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach ($Type in ($ExportableEnums + $ExportableClasses)) {
        $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'ConvertTo-PSSemVer'
        'New-PSSemVer'
    )
}
Export-ModuleMember @exports