PSPostMan.psm1

$Defaults = @{
    NugetServerUrl = 'https://www.powershellgallery.com/api/v2/package/'
    LocalNuGetExePath = "$PSScriptRoot\nuget.exe"
    NuGetExeUrl = 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe'
}

function New-Package
{
    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            if (-not (Test-Path -Path $_ -PathType Container)) {
                throw "The folder '$_' does not exist."
            } else {
                $true
            }
        })]
        [string]$FolderPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name = (Split-Path -Path $FolderPath -Leaf),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            if (-not (Test-Path -Path $_ -PathType Container)) {
                throw "The folder '$_' does not exist."
            } else {
                $true
            }
        })]
        [string]$OutputFolderPath = $FolderPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [version]$Version,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Authors,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Description,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Owners,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$LicenseUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$IconUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ReleaseNotes,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$Tags,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not (Compare-Object $_.Keys @('id','version'))) {
                throw 'One or more dependencies hashtables does not have the required keys: id and version.'
            } else {
                $true
            }
        })]
        [hashtable[]]$Dependencies,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$PassThru
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {

            if (($Version).Build -eq '-1') { $Version = "$Version.0" }

            #region Build the nuget spec
            $specParamNames = @(
                'Version',
                'Authors',
                'Owners',
                'LicenseUrl',
                'ProjectUrl',
                'IconUrl',
                'ReleaseNotes',
                'Tags',
                'Dependencies'
            )

            $tempSpecFilePath = "$env:TEMP\$Name.nuspec"
            $specParams = @{
                Name = $Name
                FilePath = $tempSpecFilePath
                Force = $true
            }
            @($specParamNames).where({ $PSBoundParameters.ContainsKey($_) }).foreach({
                $specParams[$_] = (Get-Variable -Name $_).Value
            })

            $packSpec = New-PackageSpec @specParams
            #endregion

            ## Create the nuget package
            $result = & $Defaults.LocalNuGetExePath pack $packSpec.FullName -OutputDirectory $OutputFolderPath.TrimEnd('\') -BasePath $FolderPath.TrimEnd('\')
            if (($result -join ' ') -notmatch 'Successfully created package') {
                throw $result
            } elseif ($PassThru) {
                Get-Item -Path "$OutputFolderPath\$Name.$Version.nupkg"
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        } 
        finally 
        {
            Remove-Item -Path $tempSpecFilePath -ErrorAction Ignore
        }
    }
}

function New-PackageSpec
{
    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            if ($_ -notmatch '\.nuspec$') {
                throw 'Invalid file path. Extension must be NUSPEC.'
            } else {
                $true
            }
        })]
        [string]$FilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [version]$Version = '1.0.0',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Authors = 'Adam Bertram',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Id = $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Description = $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Owners,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$LicenseUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$IconUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ReleaseNotes,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$Tags,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [hashtable[]]$Dependencies
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ((Test-Path -Path $FilePath -PathType Leaf) -and (-not $Force.IsPresent)) {
                throw "The file [$($FilePath)] already exists and -Force was not used to overwrite."
            }

            [xml]$xDoc = @"
<?xml version="1.0"?>
<package>
  <metadata>
    <id>$Id</id>
    <version>$($Version.ToString())</version>
    <authors>$Authors</authors>
    <description>$Description</description>
  </metadata>
</package>
"@


            $optionalNodes = @(
                "owners"
                "licenseUrl"
                "projectUrl"
                "iconUrl"
                "releaseNotes"
                "tags"
                "dependencies"
            )

            @($optionalNodes).where({ $PSBoundParameters.ContainsKey($_) }).foreach({
                if ($_ -eq 'Tags') {
                    $nodeName = $_ -join ' '
                } else {
                    $nodeName = $_
                }
                $nodeName = $nodeName
                $xNode = $xDoc.CreateElement($nodeName)
                if ($_ -eq 'Dependencies') {
                    @($Dependencies).foreach({
                        $xDep = $xNode.AppendChild($xDoc.CreateElement('dependency'))
                        $xDep.SetAttribute('id',$_.id)
                        $xDep.SetAttribute('version',$_.version)
                        $null = $xNode.AppendChild($xDep)
                    })
                } else {
                    $xNode.InnerText = (Get-Variable -Name $_).Value
                }
                $null = $xDoc.package.metadata.AppendChild($xNode)
            })

            $xDoc.Save($FilePath)
            Get-Item -Path $FilePath
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Publish-Package
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            if ($_ -notmatch '\.nupkg$') {
                throw 'Invalid file path. Extension must be NUPKG.'
            } else {
                $true
            }
        })]
        [string]$Path,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FeedUrl,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ApiKey,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$Timeout
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $nugetParams = [ordered]@{
                Timeout = "-timeout $Timeout"
                FeedUrl = "-source $FeedUrl"
                ApiKey = "-ApiKey $ApiKey"
            }
            $pushArgs = ''
            $nugetParams.GetEnumerator().where({$PSBoundParameters.ContainsKey($_.Key)}).foreach({
                $pushArgs += " $($_.Value)"
            })
            $pushArgs = $pushArgs.Trim()

            Write-Verbose -Message "Publishing package using Nuget args: [push `"$Path`" $pushArgs]"
            $result = Invoke-Expression -Command "& '$($Defaults.LocalNuGetExePath)' push `"$Path`" $pushArgs"
            if (-not ($result -match 'package was pushed')) {
                throw $result
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Remove-Package
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory,ParameterSetName = 'NoPipeline')]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not (Compare-Object $_.Keys @('Name','Version'))) {
                throw 'One or more hashtables in the Package parameter do not have Name/Version key/value pairs.'
            } else {
                $true
            }
        })]
        [hashtable]$PackageInfo,

        [Parameter(Mandatory,ValueFromPipeline,ParameterSetName = 'Pipeline')]
        [ValidateNotNullOrEmpty()]
        [object]$Package,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias('Source')]
        [string]$FeedUrl = $Defaults.NugetServerUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$NuGetApiKey = 'secret'
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($PSBoundParameters.ContainsKey('Package'))
            {
                $pack = @{
                    Name = $Package.Name
                    Version = $Package.Version
                }
            } elseif ($PSBoundParameters.ContainsKey('PackageInfo')) {
                $pack = @{
                    Name = $PackageInfo.Name
                    Version = $PackageInfo.Version
                }
            }
            $nuGetCli = "& $Defaults.LocalNuGetExePath delete $($pack.Name) $($pack.Version) -NonInteractive -source $FeedUrl"
            if ($NuGetApiKey) {
                $nuGetCli += " -ApiKey $NuGetApiKey"
            }
            $result = Invoke-Expression $nuGetCli
            if (($result -join ' ') -notmatch 'was deleted successfully') {
                throw $result
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Get-DependentModule
{
    [OutputType([System.Management.Automation.PSModuleInfo])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ModuleName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$Recurse
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($depModuleNames = Get-Module -Name $ModuleName -ListAvailable | Select-Object -ExpandProperty RequiredModules) {
                $depModules = Get-Module -Name $depModuleNames -ListAvailable
                if ($Recurse.IsPresent) {
                    Get-DependentModule -ModuleName $depModules.Name
                } else {
                    $depModules
                }
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function New-ModulePackage
{
    [OutputType([System.IO.FileInfo])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FolderPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$PassThru
    )
    ## TODO ADB: Get all manifest attributes
    $moduleName = ($FolderPath | Split-Path -Leaf)
    $manifest = Import-PowerShellDataFile -Path "$FolderPath\$moduleName.psd1"
    $manifestAttribToPackageMap = @{
        'ModuleVersion' = 'Version'
        'Description' = 'Description'
        'Author' = 'Authors'
        @('PrivateData','PSData','Tags') = 'Tags'
        @('PrivateData','PSData','ProjectUri') = 'ProjectUrl'
    }

    $newPackageParams = @{
        Name = $moduleName
        FolderPath = $FolderPath
        OutputFolderPath = $FolderPath
    }
    if ($PassThru.IsPresent) {
        $newPackageParams.PassThru = $true
    }

    $manifestAttribToPackageMap.GetEnumerator() | foreach {
        $val = $manifest.Clone()
        if ($_.Key -is 'array') {
            foreach ($p in $_.Key) { 
                $val = $val.$p 
            }
        } else {
            $val = $manifest.($_.Key)
        }
        $newPackageParams.($_.Value) = $val
    }
    New-PmPackage @newPackageParams
}

function Publish-Module
{
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory,ParameterSetName = 'ByName')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        [Parameter(Mandatory,ParameterSetName = 'ByFolderPath')]
        [ValidateNotNullOrEmpty()]
        [string]$FolderPath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$NuGetApiKey,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FeedUrl = $Defaults.NugetServerUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [int]$Timeout,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$PublishDependentModules
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($PSCmdlet.ParameterSetName -eq 'ByName') {
                $getModuleName = $Name
                $moduleName = $Name
            } else {
                $getModuleName = $FolderPath
                $moduleName = Split-Path -Path $FolderPath -Leaf
            }
            $modulesToPublish = Get-Module -Name $getModuleName -ListAvailable

            if (@($modulesToPublish).Count -ne @($moduleName).Count) {
                throw 'One or more modules could not be found.'
            }

            $publishPackParams = @{
                FeedUrl = $FeedUrl
                ApiKey = $NuGetApiKey
            }

            if (($depModules = Get-DependentModule -ModuleName $moduleName -Recurse) -and (-not $PublishDependentModules.IsPresent)) {
                throw "The module(s) [$($moduleName -join ',')] have dependent module(s) [$($depModules.Name -join ',')]. Use -PublishDependentModules to publish these as well."
            } else {
                @($depModules).foreach({
                    if (-not (Test-ModuleExists -Name $_.Name))
                    {
                        throw "The dependenent module [$($_.Name)] needs to be published but was not found."
                    }
                    else
                    {
                        Write-Verbose -Message "Creating package for module [$($_.Name)]..."
                        $pkg = New-PmPackage -FolderPath $_.ModuleBase -PassThru -Version $_.Version
                        Publish-PmPackage @publishPackParams -Path $pkg.FullName
                        Remove-Item -Path $pkg.FullName -ErrorAction Ignore
                    }
                })
            }

            @($modulesToPublish).foreach({
                $newPkgParams = @{
                    FolderPath = $_.ModuleBase
                    PassThru = $true
                }
                if ($depModules) {
                    $newPkgParams.Dependencies = @($depModules).foreach({
                        @{id=$_.Name;version=$_.Version}
                    })
                }
                $pkg = New-PmModulePackage @newPkgParams
                Publish-PmPackage @publishPackParams -Path $pkg.FullName
            })
            
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        } finally {
            Remove-Item -Path $pkg.FullName -ErrorAction Ignore
        }
    }
}

function Test-ModuleExists
{
    [OutputType([bool])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if (Get-Module -Name $Name -ListAvailable) {
                $true
            } else {
                $false
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Find-Package
{
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FeedUrl = $Defaults.NugetServerUrl

    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                $whereFilter = { $_ -match "^$($Name -join '|')" }
            } else {
                $whereFilter = { $_ }
            }

            $packageList = @(& $Defaults.LocalNuGetExePath list -Source $FeedUrl).where($whereFilter)
            if ($packageList -notmatch 'no packages found') {
                @($packageList).foreach({
                    $split = $_.Split(' ') 
                    $version = $split[-1]
                    if ($split.Count -eq 2) { 
                        $packageName = $split[0] 
                    } else { 
                        $packageName = $split[0..-2] -join ' '
                    } 
                    [pscustomobject]@{Name = $packageName; Version = $version}  
                })
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

function Publish-DscResource
{
    ## TODO: Add pipeline support for Get-DscResource at some point
    [OutputType([void])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            if (-not (Get-DscResource -Name $_ -ErrorAction Ignore)) {
                throw "The DSC resource [$($_)] was not found"
            } else {
                $true
            }

         })]
        [string[]]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FeedUrl = $Defaults.NugetServerUrl,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$NuGetApiKey = 'secret',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$PublishDependentModules
    )
    begin
    {
        $ErrorActionPreference = 'Stop'
    }
    process
    {
        try
        {
            $publishPackParams = @{
                FeedUrl = $FeedUrl
            }
            if ($NuGetApiKey)
            {
                $publishPackParams.ApiKey = $NuGetApiKey
            }
            
            ## TODO: Need to group these dependency checks together if multiple resources are passed so the same thing
            ## isn't done for every resource. Could also makes these parallel
            
            ## Ensure any and all dependent modules are available before proceeding
            @($Name).foreach({
                $resourceName = $_
                $resourceModule = Get-Module -Name (Get-DscResource -Name $resourceName).ModuleName -ListAvailable
                Write-Verbose -Message "The DSC resource [$($resourceName)] is in the module [$($resourceModule.Name)]"
                if ($dscModuleDeps = Get-DependentModule -ModuleName $resourceModule.Name) {
                    Write-Verbose -Message "Found [$($dscModuleDeps.Count)] dependent module(s)..."
                        
                    $depModulesInFeed = Find-Package -Name $dscModuleDeps.Name
                    @($dscModuleDeps).foreach({
                        if ($_.Name -notin $depModulesInFeed.Name)
                        {
                            if (-not $PublishDependentModules.IsPresent)
                            {
                                throw "The dependent module [$($_.Name)] is not published to the feed specified. Downloading this module will fail if uploaded now. Use -PublishDependentModules."
                            }
                            else
                            {
                                if (-not (Test-ModuleExists -Name $_.Name))
                                {
                                    throw "The dependenent module [$($_.Name)] needs to be published but was not found."
                                }
                                else
                                {
                                    Publish-Module -FeedUrl $FeedUrl -Name $_.Name -
                                    Write-Verbose -Message "Creating package for module [$($_.Name)]..."
                                    $pkg = New-PmPackage -FolderPath $_.ModuleBase -PassThru -Version $_.Version
                                    Publish-PmPackage @publishPackParams -Path $pkg.FullName
                                    Remove-Item -Path $pkg.FullName -ErrorAction Ignore
                                }
                            }
                        }
                                
                    })
                }
                $newPkgParams = @{
                    FolderPath = $resourceModule.ModuleBase
                    PassThru = $true
                    Version = $resourceModule.Version
                    Tags = "PsDscResource_$resourceName" ## Required for Find-DscResource to find the module
                }
                if ($dscModuleDeps) {
                    $newPkgParams.Dependencies = @($dscModuleDeps).foreach({
                        @{id=$_.Name;version=$_.Version}
                    })
                }
                $pkg = New-PmPackage @newPkgParams
                Publish-PmPackage @publishPackParams -Path $pkg.FullName
                Remove-Item -Path $pkg.FullName -ErrorAction Ignore
            })
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}