cliHelper.semver.psm1

#!/usr/bin/env pwsh
using namespace System
using namespace System.management.automation
using namespace System.ComponentModel.TypeConverter
using namespace Microsoft.PackageManagement.Provider.Utility

#region Classes
enum VersionComponent {
  Major
  Minor
  Patch
  PreReleaseIdentifier
  BuildMetadata
}

enum PrecedenceComparisonResult {
  Lower
  Equal
  Higher
}


<#
.SYNOPSIS
  Main class
.EXAMPLE
  # Parse a version
  $semver = [Semanticver]::Parse("1.2.3-alpha+build123")

  # Increment the minor version
  $incrementedSemver = [Semanticver]::Increment($semver, [VersionComponent]::Minor)
  Write-Host "Incremented Minor: $($incrementedSemver.ToString())" # Output: 1.3.0

  # Get the Patch component
  $patchVersion = [Semanticver]::GetComponent($semver, [VersionComponent]::Patch)
  Write-Host "Patch Version: $patchVersion" # Output: 3

  # Parse another version for comparison
  $semver2 = [Semanticver]::Parse("1.3.0")

  # Compare Minor components
  $comparisonResult = [Semanticver]::CompareComponent($semver, $semver2, [VersionComponent]::Minor)
  Write-Host "Minor Component Comparison: $comparisonResult" # Output: Lower (because 1.2.3 vs 1.3.0)
#>

class Semanticver {
  [int]$Major
  [int]$Minor
  [string]$Build
  [int]$Patch
  [string]$PreRelease
  static [string]$SemanticVersionPattern = '^(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-]+)*))?$'
  Semanticver() {
    $this.Major = 0
    $this.Minor = 0
    $this.Patch = 0
    $this.PreRelease = ""
    $this.Build = ""
  }

  static [Semanticver] Parse([string]$versionString) {
    if (![Semanticver]::IsValid($versionString)) {
      throw "Invalid SemVer string: '$versionString'"
    }
    $local:matches = @{}
    if ($versionString -match [regex]::Escape([Semanticver]::SemanticVersionPattern) -and $matches.Count -gt 0) {
      $semver = [Semanticver]::new()
      $semver.Major = [int]::Parse($matches.1)
      $semver.Minor = [int]::Parse($matches.2)
      $semver.Patch = [int]::Parse($matches.3)
      $semver.PreRelease = $matches.4
      $semver.Build = $matches.5
      return $semver
    }
    # Should not reach here if IsValid is working correctly, but for safety:
    throw "Failed to parse SemVer string: '$versionString'"
  }

  # checks if a string is a valid semantic version
  static [bool] IsValid([string]$versionString) {
    return $versionString -match [regex]::Escape([Semanticver]::SemanticVersionPattern)
  }

  [string] ToString() {
    $version = "$($this.Major).$($this.Minor).$($this.Patch)"
    if (![string]::IsNullOrEmpty($this.PreRelease)) {
      $version += "-$($this.PreRelease)"
    }
    if (![string]::IsNullOrEmpty($this.Build)) {
      $version += "+$($this.Build)"
    }
    return $version
  }
  # Increments a version component
  static [Semanticver] Increment([Semanticver]$version, [VersionComponent]$component) {
    $newVersion = [Semanticver]::Parse($version.ToString()) # Create a copy to avoid modifying the original object
    switch ($component.ToString()) {
      "Major" {
        $newVersion.Major++
        $newVersion.Minor = 0
        $newVersion.Patch = 0
        $newVersion.PreRelease = ""
      }
      "Minor" {
        $newVersion.Minor++
        $newVersion.Patch = 0
        $newVersion.PreRelease = ""
      }
      "Patch" {
        $newVersion.Patch++
        $newVersion.PreRelease = ""
      }
      "PreReleaseIdentifier" {
        # For now, simply clear prerelease. More sophisticated logic can be added later.
        $newVersion.PreRelease = ""
      }
      "BuildMetadata" {
        # Build metadata is usually not incremented in SemVer, but you could add logic here if needed.
        # For now, we'll leave it as is.
      }
      default {
        throw "Unsupported VersionComponent for Increment: '$component'"
      }
    }
    return $newVersion
  }
  static [object] GetComponent([Semanticver]$version, [VersionComponent]$component) {
    $result = switch ($component.ToString()) {
      "Major" { $version.Major; break }
      "Minor" { $version.Minor; break }
      "Patch" { $version.Patch; break }
      "PreReleaseIdentifier" { $version.PreRelease; break }
      "BuildMetadata" { $version.Build; break }
      default {
        throw "Unsupported VersionComponent for GetComponent: '$component'"
      }
    }
    return $result
  }

  # Compares a specific version component
  static [PrecedenceComparisonResult] CompareComponent([Semanticver]$version1, [Semanticver]$version2, [VersionComponent]$component) {
    $result = switch ($component.ToString()) {
      "Major" {
        if ($version1.Major -lt $version2.Major) { "Lower" }
        elseif ($version1.Major -gt $version2.Major) { "Higher" }
        else { "Equal" }
        break
      }
      "Minor" {
        if ($version1.Minor -lt $version2.Minor) { "Lower" }
        elseif ($version1.Minor -gt $version2.Minor) { "Higher" }
        else { "Equal" }
        break
      }
      "Patch" {
        if ($version1.Patch -lt $version2.Patch) { "Lower" }
        elseif ($version1.Patch -gt $version2.Patch) { "Higher" }
        else { "Equal" }
        break
      }
      "PreReleaseIdentifier" {
        # For now, simple string comparison. More complex SemVer prerelease comparison can be added.
        $comparison = [string]::Compare($version1.PreRelease, $version2.PreRelease, [StringComparer]::InvariantCultureIgnoreCase)
        if ($comparison -lt 0) { "Lower" }
        elseif ($comparison -gt 0) { "Higher" }
        else { "Equal" }
        break
      }
      "BuildMetadata" {
        # Build metadata is not considered in SemVer precedence, so always Equal for comparison purposes in this context.
        "Equal"
        break
      }
      default {
        throw "Unsupported VersionComponent for CompareComponent: '$component'"
      }
    }
    return $result
  }
}

#endregion Classes

# Types that will be available to users when they import the module.
$typestoExport = @(
  [Semanticver], [VersionComponent], [PrecedenceComparisonResult]
)
$TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
foreach ($Type in $typestoExport) {
  if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) {
    $Message = @(
      "Unable to register type accelerator '$($Type.FullName)'"
      'Accelerator already exists.'
    ) -join ' - '
    "TypeAcceleratorAlreadyExists $Message" | Write-Debug
  }
}
# Add type accelerators for every exportable type.
foreach ($Type in $typestoExport) {
  $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
  foreach ($Type in $typestoExport) {
    $TypeAcceleratorsClass::Remove($Type.FullName)
  }
}.GetNewClosure();

$scripts = @();
$Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += $Public

foreach ($file in $scripts) {
  Try {
    if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue }
    . "$($file.fullname)"
  } Catch {
    Write-Warning "Failed to import function $($file.BaseName): $_"
    $host.UI.WriteErrorLine($_)
  }
}

$Param = @{
  Function = $Public.BaseName
  Cmdlet   = '*'
  Alias    = '*'
  Verbose  = $false
}
Export-ModuleMember @Param