Resource/Resource.ps1

#region Copyright & License

# Copyright © 2012 - 2021 François Chabot
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#endregion

Set-StrictMode -Version Latest

function Get-ResourceItem {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo[]])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FolderPath = $MyInvocation.PSScriptRoot,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Extension = @(".dll", ".exe")
    )
    Process {
        $Name | ForEach-Object -Process { $_ } -PipelineVariable currentName | ForEach-Object -Process {
            $items = Get-ChildItem -Path $FolderPath -File -Recurse | Where-Object -FilterScript { $_.BaseName -eq $currentName -and $_.Extension -in $Extension } | Get-Item
            if ($items | Test-None) {
                throw "Resource item not found [Path: '$FolderPath', Name: '$currentName', Extension = '$($Extension -join ", ")']."
            }
            $duplicateItems = $items | Group-Object Name | Where-Object Count -GT 1
            if ($duplicateItems | Test-Any) {
                throw "Ambiguous resource items found ['$($duplicateItems.Name -join "', '")'] matching criteria [Path: '$FolderPath', Name: '$currentName', Extensions = '$($Extension -join ", ")']."
            }
            $items
        }
    }
}

function New-ResourceItem {
    [CmdletBinding(DefaultParameterSetName = 'named-resource')]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $true, ParameterSetName = 'file-resource')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Resource,

        [Parameter(Mandatory = $true, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $false, ParameterSetName = 'file-resource')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'file-resource')]
        [ValidateScript( { $_ | Test-Path -PathType Leaf } )]
        [psobject[]]
        $Path,

        [Parameter(Mandatory = $false, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $false, ParameterSetName = 'file-resource')]
        [ValidateScript( { $_ -is [bool] -or $_ -is [ScriptBlock] } )]
        [ValidateNotNullOrEmpty()]
        [psobject]
        $Condition = $true,

        [Parameter(Mandatory = $false, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $false, ParameterSetName = 'file-resource')]
        [switch]
        $PassThru,

        [Parameter(DontShow, Mandatory = $false, ParameterSetName = 'named-resource', ValueFromRemainingArguments = $true)]
        [Parameter(DontShow, Mandatory = $false, ParameterSetName = 'file-resource', ValueFromRemainingArguments = $true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [object[]]
        $UnboundArguments = @()
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $splattedArguments = ConvertTo-SplattedArguments -UnboundArguments $UnboundArguments
    # any item is assumed to be included by default unless scpecified otherwise: when its Condition is either $false or deferred (a ScriptBlock)
    if ($Condition -is [ScriptBlock] -or -not($Condition)) { $splattedArguments.Add('Condition', $Condition) }

    $(if ($PSCmdlet.ParameterSetName -eq 'named-resource') { $Name } else { $Path | Resolve-Path | Select-Object -ExpandProperty ProviderPath }) | ForEach-Object -Process {
        $item = New-Object -TypeName PSCustomObject
        if ($PSCmdlet.ParameterSetName -eq 'named-resource') {
            Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value $_
        } else {
            Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value $(if ($Name | Test-Any) { $Name } else { Split-Path -Path $_ -Leaf })
            Add-Member -InputObject $item -MemberType NoteProperty -Name Path -Value $_
        }
        Add-ResourceItemMembers -Item $item -Members $splattedArguments
        if ($PassThru) {
            $item
        } else {
            # TODO support $ItemUnicityScope
            # TODO write-verbose no matter the ItemUnicityScope
            # TODO ?? write-error about Item redefinition according to the ItemUnicityScope,
            # unicity => where Path is the unique criterium xor all the properties must be unique
            # TODO ensure resource requirement an application manifest are only added to such a manifest

            # only add items whose condition is either $true or deferred (ScriptBlock)
            if ($Condition -is [ScriptBlock] -or $Condition) {
                if ($Manifest.ContainsKey($Resource)) {
                    $Manifest.$Resource = @($Manifest.$Resource) + $item
                } else {
                    $Manifest.Add($Resource, $item)
                }
            }
        }
    }
}

function New-ResourceManifest {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [string]
        $Description,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Manifest', 'Resource', 'None')]
        [string]
        $ItemUnicityScope = 'Manifest',

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]
        $Build,

        [Parameter(DontShow, Mandatory = $false, ValueFromRemainingArguments = $true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [object[]]
        $UnboundArguments = @()
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    $item = New-Object -TypeName PSCustomObject
    Add-Member -InputObject $item -MemberType NoteProperty -Name Type -Value $Type
    Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value $Name
    Add-Member -InputObject $item -MemberType NoteProperty -Name Description -Value $Description
    Add-ResourceItemMembers -Item $item -Members (ConvertTo-SplattedArguments -UnboundArguments $UnboundArguments)

    $manifestBuildScript = [scriptblock] {
        [CmdletBinding()]
        [OutputType([void])]
        param (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [hashtable]
            $Manifest
        )
        . $Build
    }

    $manifest = @{ }
    $manifest.Add('Properties', $item)
    & $manifestBuildScript -Manifest $manifest
    $manifest
}

#region helpers

function Add-ResourceItemMembers {
    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSCustomObject]
        $Item,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowEmptyCollection()]
        [hashtable]
        $Members
    )
    Process {
        $Members.Keys | ForEach-Object -Process {
            if ($Members.$_ -is [ScriptBlock]) {
                # ScriptMethod instead of ScriptProperty to avoid any error to be silenced; see https://stackoverflow.com/a/19777735/1789441
                Add-Member -InputObject $item -MemberType ScriptMethod -Name $_ -Value $Members.$_
            } else {
                Add-Member -InputObject $item -MemberType NoteProperty -Name $_ -Value $Members.$_
            }
        }
    }
}

function Compare-ResourceItem {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.GetType().Name -eq 'PSCustomObject' })]
        [PSCustomObject]
        $ReferenceItem,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]
        $DifferenceItem
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    $referenceProperties = @(Get-Member -InputObject $ReferenceItem -MemberType  NoteProperty, ScriptProperty | Select-Object -ExpandProperty Name)
    $differenceProperties = @(Get-Member -InputObject $DifferenceItem -MemberType  NoteProperty, ScriptProperty | Select-Object -ExpandProperty Name)
    $referenceProperties + $differenceProperties | Select-Object -Unique -PipelineVariable key | ForEach-Object -Process {
        if ($referenceProperties.Contains($key) -and !$differenceProperties.Contains($key)) {
            [PSCustomObject]@{Property = $key ; ReferenceValue = $ReferenceItem.$key ; SideIndicator = '<' ; DifferenceValue = $null } | Tee-Object -Variable difference
            Write-Verbose -Message $difference
        } elseif (!$referenceProperties.Contains($key) -and $differenceProperties.Contains($key)) {
            [PSCustomObject]@{Property = $key ; ReferenceValue = $null ; SideIndicator = '>' ; DifferenceValue = $DifferenceItem.$key } | Tee-Object -Variable difference
            Write-Verbose -Message $difference
        } else {
            $referenceValue, $differenceValue = $ReferenceItem.$key, $DifferenceItem.$key
            if ($referenceValue -is [array] -and $differenceValue -is [array]) {
                $arrayDifferences = Compare-Object -ReferenceObject $referenceValue -DifferenceObject $differenceValue
                if ($arrayDifferences | Test-Any) {
                    $uniqueReferenceValues = $arrayDifferences | Where-Object -FilterScript { $_.SideIndicator -eq '<=' } | ForEach-Object -Process { $_.InputObject } | Join-String -Separator ", "
                    $uniqueDifferenceValues = $arrayDifferences | Where-Object -FilterScript { $_.SideIndicator -eq '=>' } | ForEach-Object -Process { $_.InputObject } | Join-String -Separator ", "
                    [PSCustomObject]@{Property = $key ; ReferenceValue = "($uniqueReferenceValues)" ; SideIndicator = '<>' ; DifferenceValue = "($uniqueDifferenceValues)" } | Tee-Object -Variable difference
                    Write-Verbose -Message $difference
                }
            } elseif ($referenceValue -is [hashtable] -and $differenceValue -is [hashtable]) {
                Compare-HashTable -ReferenceHashTable $referenceValue -DifferenceHashTable $differenceValue -Prefix "$Key"
            } elseif ($referenceValue -ne $differenceValue) {
                [PSCustomObject]@{Property = $key ; ReferenceValue = $referenceValue ; SideIndicator = '<>' ; DifferenceValue = $differenceValue } | Tee-Object -Variable difference
                Write-Verbose -Message $difference
            }
        }
    }
}

function ConvertTo-SplattedArguments {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowEmptyCollection()]
        [psobject[]]
        $UnboundArguments
    )
    $splattedArguments = @{ }
    $UnboundArguments | ForEach-Object -Process {
        if ($_ -is [array]) {
            $splattedArguments.$lastParameterName = $_
        } else {
            switch -regex ($_) {
                # parse parameter name
                '^-(\w+):?$' {
                    $splattedArguments.Add(($lastParameterName = $matches[1]), $null)
                    break
                }
                # parse values of last parsed parameter
                default {
                    $splattedArguments.$lastParameterName = $_
                    break
                }
            }
        }
    }
    $splattedArguments
}

#endregion
# SIG # Begin signature block
# MIIJEgYJKoZIhvcNAQcCoIIJAzCCCP8CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU47E9o7jdq25B9nmjsZjsKHUE
# ncKgggWhMIIFnTCCA1GgAwIBAgIQKBOAjgMDO55A7UJ/k/g5nTBBBgkqhkiG9w0B
# AQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQC
# AQUAogMCASAwJjEkMCIGA1UEAwwbaWNyYWZ0c29mdHdhcmVAc3RhdGVsZXNzLmJl
# MB4XDTIwMDYyMzExNDM1NloXDTIxMDYyMzEyMDM1NlowJjEkMCIGA1UEAwwbaWNy
# YWZ0c29mdHdhcmVAc3RhdGVsZXNzLmJlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEAmQcb0GwlBHBHBJZ9vNM8EewN7T+nhsWVU0WBoWnIw6UAT99Rw9x5
# RcfOQU2hxqKmR1k+iI6B+qddpTC3VLSChA/mh1P4pCDDsZeyR/0nn/r/DezhDe8x
# 5jckjR88KSRcgDoh0kLjgfrToDpx9EvBcwXmNJKDwBIWu5SBvk04beU4XO7OHjBo
# g0kMaHxCZc9HcWfdzBefP+fbVzu6f1j1WgEqZn9sr1ML2ulHRdu26+56xGq9RZGJ
# vXyY1mY+K5mqBcET+1bV2pZnBrM3Gc/hlmvTkwrC0ZGBALLZWZqqpLVrDCY5eoHP
# w2C0kA4JzK4Q1o218s+wXbuDcjYRIZqBSwI8fizR/4DS+6dEjfa3kzs2z/MrkJOk
# hJ06tiMSRr55tX1DR8NwVLdiNqZYvs4zP2ZNRMMI8uFCjkP/Wn1hfBr+GSPlgdLq
# 2TFishY2pj5O1WlE/tCz+B0YLhPWdfbVEp8kB3fGBsVf7uw4STK/wDA1MYRIHikt
# w+K9gtdf0eIR9dYX9CMwoDN2TNLK6vnCWMrzWFe5EOU3/oljUBkyQT838a5A6wMu
# cGeu7Cwjdigylt7ULaTglL7ORIyaRbzkltxd+1oaQ21kjl4ef0ZD2gWLj7bwrZR+
# KWCfmaHFoZlVRKNPtScuyOnilPGGZ6T7SNuwVxSXFRtbp+cQea4UxxUCAwEAAaNf
# MF0wDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBcGA1UdEQQQ
# MA6CDHN0YXRlbGVzcy5iZTAdBgNVHQ4EFgQUq4sCoE2IqN4K4uwNuibjqd5yNNQw
# QQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDAN
# BglghkgBZQMEAgEFAKIDAgEgA4ICAQBR98amLpANKFlc7mPlkaV4ZtS2uTmbJ6dO
# qzyWKJ2yTmv7U9yq8PdEH9mPJlxYvGyNgxqHoocKv1SdjgYh27SM8pDnsfU2NpER
# 6K/3sICy6Orh9vhC+U18Bp93WoLEezolaBcF0co3/o+HazOvs/2zBFONFHMkef9/
# 3Bipm0sd95teHo53vLKViHbjSmoGxYsvJJiYITB4Zeo6xgUAmwcUpL1To62Lb3RP
# CDLKZQ5h8Ir07nncV4HLq+0qF3+G9Y0IXHJv6Qcr/XTTLo0J877HRqS37WJcgF8+
# 2nbZbqO9NVvp14A4nTqpeDFmzewDU33hiZvzuLHBj//OgLgGZ9lJPxCu0tVxfFWZ
# INHg1YHp3lMaAw00Q3tb/vhc5kE6Kl7FnXnUTsu4j+vUoaFMWhYezoyn9m4rD+xN
# RITrbLPZdWAZvVOJ8ehmswRhfiMZ1npwbrk7KU1UTsmMS7PHREWSyUM28WlMFf2i
# ut8TlY/MV/adUGr2GpqBWhxp5DRgfl1uamKm2wFlCra3/kReVlQgC/Bbod2JOgJW
# t8zCbO4nJx+fJYwM9RG70h/TmuqzP8uChsHtKcgs2YtXmSm12JZakXY4IflInI7p
# ddDEs9UOfsWXDsqpvmFQZbwgGeNeEsPk3Fdm1MzDtS9PBXMk4jGGXNzEsVUgwf42
# 2HuDWeX/4jGCAtswggLXAgEBMDowJjEkMCIGA1UEAwwbaWNyYWZ0c29mdHdhcmVA
# c3RhdGVsZXNzLmJlAhAoE4COAwM7nkDtQn+T+DmdMAkGBSsOAwIaBQCgeDAYBgor
# BgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEE
# MBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBQh
# CSQe/hE+58loLkcm9xaWg/3AhjANBgkqhkiG9w0BAQEFAASCAgBWAENyuKo0SOru
# VC8Fs9yqRO+mj+r4wJ/5EE5SnHbJ1H55DcLglILL3xid4Y937cEQh5e2QdgRNY+J
# bS1+MqZYykKaKkOjn1c+YGu5gvvha032W0FwsazJQz4osuO8CtoXlZXuh/ENufFL
# DSoMyS5yHsh0SkC1dsV4EtViAu7TdlssVZXMAiKEgo6fs0lQQ2lurvU0Tkl81Hf9
# c1PoqmmpQ3UKFcQeGXyLE53St6TihDWURHf8sqLjHJgW8FJQOTlxpTEZBwjDnx96
# pXlmg105sLVwNIDZEQVPQEjkxqamZrpYEs6ewjp4c8nWEAj6tZgGQQ5sbC0VKpJ9
# ADi93ZtPh2GrDJ4yoEDDbNaQMvfb43FMu/tYzmC4YU5/91x+W+Yg7LUpukN29kLC
# jokb7OyLg/d+lv/I2XbClzpYjoGQlfPIWwzSMxqufMJxmiHvYq025pkcTyOykz2d
# NF/xuS3XNY73OY4Q3Le4waJbTS0K4+nOjVb2s0tBj1gjVEg3inxefbWJn33MjLNQ
# jAnyAfq/UOkPibNQBtK7iZ30Q57GuOe5luyR5v+AJceY0oEtY27QpBXAhnwZxprB
# az6ttfaIcVPHy5x/czg3oKIyhE+T6VvdUCX774qUcBA0Jc0tv/wSUJq+vptOSOZ9
# UBrBv7Z49W+os3jg/NjPKMabQP1v0w==
# SIG # End signature block