├── .gitignore ├── LICENSE ├── PSScriptAnalyzerSettings.psd1 ├── README.md ├── functional.psd1 ├── functional.psm1 └── functional.tests.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Kuech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 'ExcludeRules' = @( 3 | 'PSAvoidUsingPositionalParameters' 4 | ) 5 | 'Rules' = @{ 6 | 'PSAvoidUsingCmdletAliases' = @{ 7 | 'Whitelist' = @( 8 | '?', 9 | '%', 10 | 'foreach', 11 | 'group', 12 | 'measure', 13 | 'select', 14 | 'where' 15 | ) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functional 2 | A small functional programming utility module for PowerShell. Read [Functional Programming in PowerShell](https://medium.com/swlh/functional-programming-in-powershell-876edde1aadb?source=friends_link&sk=e684adbee39b5c936e44e59a1126f4eb) to learn more about how to apply functional programming paradigm to PowerShell, as well as see a more conceptual explanation and examples of using this module. 3 | 4 | ## How does functional programming work in PowerShell? 5 | 6 | ### Processing arrays in pipelines with functions 7 | Functional Programming is largely about applying functions to Lists to obtain different results. PowerShell has good support for this as long as the function only takes a single argument, such as with filter functions and their implicit `$_` argument. 8 | 9 | ```PowerShell 10 | # square each number 11 | 1..10 | % {$_ * $_} 12 | ``` 13 | 14 | ### Functions as first-class objects 15 | Functional Programming languages all support functions as first-class objects, which basically means that the function can be assigned to a variable. PowerShell does not support functions as first-class objects—so how can PowerShell support functional programming? 16 | 17 | PowerShell supports something similar to functions, called `scriptblock`s, which *are* first-class objects. 18 | 19 | ```PowerShell 20 | # define a scriptblock 21 | $add = { 22 | Param($a, $b) 23 | $a + $b 24 | } 25 | ``` 26 | 27 | You can also obtain a `scriptblock` from a PowerShell function. 28 | 29 | ```PowerShell 30 | # reference the scriptblock from a function 31 | function add($a, $b) { 32 | $a + $b 33 | } 34 | $add = $function:add 35 | ``` 36 | 37 | Or from a string. 38 | ```PowerShell 39 | # load a script as a scriptblock 40 | $add = [scriptblock](Get-Content add.ps1 -Raw) 41 | ``` 42 | 43 | You can invoke your scriptblock with read/write access to variables in your current scope using `.` or with only read access to your current scope using `&`. 44 | 45 | ```PowerShell 46 | $n = 14 47 | function addToN($a) { 48 | $n = $a + $n 49 | } 50 | 51 | &$function:addToN 3 52 | $n # `14` 53 | .$function:addToN 3 54 | $n # `17` 55 | ``` 56 | 57 | ## How does this module help with functional programming? 58 | 59 | Functional Programming is largely about applying functions to Lists to obtain different results. PowerShell pipelines have amazing support for this, but of the three main Functional Programming functions, PowerShell only has support for two. 60 | 61 | | Input | Output | Python function | PowerShell function | 62 | |-|-|-|-| 63 | | List, function | List of same length | `map` | `ForEach-Object` | 64 | | List, function | List of smaller length | `filter` | `Where-Object` | 65 | | List, function | Any type | `reduce` | *?* | 66 | 67 | This module introduces five cmdlets for functional programming: 68 | * `Reduce-Object`, for applying a function to each element of the array and an accumulated value, and returning the acculated value. 69 | * `Merge-Object`, for recursively merging two objects using a given strategy or a custom strategy. 70 | * `Test-All`, for testing if all elements are truthy 71 | * `Test-Any`, for testing if any elements are truthy 72 | * `Test-Equality`, for recursively testing for deep equality 73 | 74 | ```PowerShell 75 | 1..10 | Reduce-Object {$a + $b} 76 | # outputs `55` 77 | 78 | Merge-Object @{a = @{b = 1}} @{a = @{c = 2}} -Strategy Fail 79 | # outputs `@{a = @{b = 1; c = 2}}` 80 | ``` 81 | 82 | ## Where's the rest of the docs? 83 | This PowerShell module is pure PowerShell (not C#) so there are some limitations with using `help`, but generally, the best way to learn about the cmdlets in the module is to install the module with `Install-Module functional` and use the `help` cmdlet to learn more about each cmdlet. 84 | 85 | Feel free to file an issue to request specific documentation improvements (or feature requests, bugs, etc). 86 | -------------------------------------------------------------------------------- /functional.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'functional' 3 | # 4 | # Generated by: kuech 5 | # 6 | # Generated on: 5/23/19 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'functional.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.0.4' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '91dd1b87-d0cf-4831-8806-3e5d1033c857' 22 | 23 | # Author of this module 24 | Author = 'Christopher Kuech' 25 | 26 | # Company or vendor of this module 27 | # CompanyName = 'Unknown' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Christopher Kuech. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Functional Programming utilities for PowerShell' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @( 73 | 'Merge-Object', 74 | 'Merge-ScriptBlock', 75 | 'Reduce-Object', 76 | 'Test-All', 77 | 'Test-Any', 78 | 'Test-Equality' 79 | ) 80 | 81 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 82 | CmdletsToExport = @() 83 | 84 | # Variables to export from this module 85 | VariablesToExport = @() 86 | 87 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 88 | AliasesToExport = @( 89 | 'compose', 90 | 'exists', 91 | 'forall', 92 | 'merge', 93 | 'reduce' 94 | ) 95 | 96 | # DSC resources to export from this module 97 | # DscResourcesToExport = @() 98 | 99 | # List of all modules packaged with this module 100 | # ModuleList = @() 101 | 102 | # List of all files packaged with this module 103 | # FileList = @() 104 | 105 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 106 | PrivateData = @{ 107 | 108 | PSData = @{ 109 | 110 | # Tags applied to this module. These help with module discovery in online galleries. 111 | # Tags = @() 112 | 113 | # A URL to the license for this module. 114 | # LicenseUri = '' 115 | 116 | # A URL to the main website for this project. 117 | # ProjectUri = '' 118 | 119 | # A URL to an icon representing this module. 120 | # IconUri = '' 121 | 122 | # ReleaseNotes of this module 123 | # ReleaseNotes = '' 124 | 125 | } # End of PSData hashtable 126 | 127 | } # End of PrivateData hashtable 128 | 129 | # HelpInfo URI of this module 130 | # HelpInfoURI = '' 131 | 132 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 133 | # DefaultCommandPrefix = '' 134 | 135 | } 136 | 137 | -------------------------------------------------------------------------------- /functional.psm1: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # enums 4 | # 5 | 6 | enum MergeStrategy { 7 | Add 8 | Override 9 | Fail 10 | } 11 | 12 | enum ParamStyle { 13 | Implicit 14 | Explicit 15 | Infer 16 | } 17 | 18 | enum Direction { 19 | Left 20 | Right 21 | } 22 | 23 | ## 24 | # module variables 25 | # 26 | 27 | $Strategies = @{ 28 | Add = { 29 | Param($a, $b) 30 | return $a + $b 31 | } 32 | Override = { 33 | Param($a, $b) 34 | return $b 35 | } 36 | Fail = { 37 | Param($a, $b) 38 | throw "Cannot merge type '$($a.GetType())' and '$($b.GetType())'" 39 | } 40 | } 41 | 42 | ## 43 | # helper functions 44 | # 45 | 46 | # don't use `-is [PSCustomObject]` 47 | # https://github.com/PowerShell/PowerShell/issues/9557 48 | function isPsCustomObject($v) { 49 | $v.PSTypeNames -contains 'System.Management.Automation.PSCustomObject' 50 | } 51 | 52 | # recursively test $a and $b for deep equality 53 | function recursiveEquality($a, $b) { 54 | if ($a -is [array] -and $b -is [array]) { 55 | Write-Debug "recursively test arrays '$a' '$b'" 56 | if ($a.Count -ne $b.Count) { 57 | return $false 58 | } 59 | $inequalIndexes = 0..($a.Count - 1) | ? { -not (recursiveEquality $a[$_] $b[$_]) } 60 | return $inequalIndexes.Count -eq 0 61 | } 62 | if ($a -is [hashtable] -and $b -is [hashtable]) { 63 | Write-Debug "recursively test hashtable '$a' '$b'" 64 | $inequalKeys = $a.Keys + $b.Keys ` 65 | | Sort-Object -Unique ` 66 | | ? { -not (recursiveEquality $a[$_] $b[$_]) } 67 | return $inequalKeys.Count -eq 0 68 | } 69 | if ((isPsCustomObject $a) -and (isPsCustomObject $b)) { 70 | Write-Debug "a is pscustomobject: $($a -is [psobject])" 71 | Write-Debug "recursively test objects '$a' '$b'" 72 | $inequalKeys = $a.psobject.Properties + $b.psobject.Properties ` 73 | | % Name ` 74 | | Sort-Object -Unique ` 75 | | ? { -not (recursiveEquality $a.$_ $b.$_) } 76 | return $inequalKeys.Count -eq 0 77 | } 78 | Write-Debug "test leaves '$a' '$b'" 79 | return (($null -eq $a -and $null -eq $b) -or ($null -ne $a -and $null -ne $b -and $a.GetType() -eq $b.GetType() -and $a -eq $b)) 80 | } 81 | 82 | # merge `$a` and `$b` recursively. If `$a` and `$b` cannot be merged, 83 | # pass `$a` and `$b` to `$strategy` to resolve the conflict. 84 | function recursiveMerge($a, $b, [scriptblock]$strategy) { 85 | if ($null -eq $a) { 86 | Write-Debug "new assignment '$b'" 87 | return $b 88 | } 89 | if ($a -eq $b -or $null -eq $b) { 90 | Write-Debug "existing assignment '$a'" 91 | return $a 92 | } 93 | if ($a -is [array] -and $b -is [array]) { 94 | Write-Debug "merge arrays '$a' '$b'" 95 | return $a + $b | Sort-Object -Unique 96 | } 97 | if ($a -is [hashtable] -and $b -is [hashtable]) { 98 | Write-Debug "merge hashtable '$a' '$b'" 99 | $merged = @{ } 100 | $a.Keys + $b.Keys ` 101 | | Sort-Object -Unique ` 102 | | % { $merged[$_] = recursiveMerge $a[$_] $b[$_] $strategy } 103 | return $merged 104 | } 105 | if ((isPsCustomObject $a) -and (isPsCustomObject $b)) { 106 | Write-Debug "a is pscustomobject: $($a -is [psobject])" 107 | Write-Debug "merge objects '$a' '$b'" 108 | $merged = @{ } 109 | $a.psobject.Properties + $b.psobject.Properties ` 110 | | % Name ` 111 | | Sort-Object -Unique ` 112 | | % { $merged[$_] = recursiveMerge $a.$_ $b.$_ $strategy } 113 | return [PSCustomObject]$merged 114 | } 115 | Write-Debug "resolve conflict '$a' '$b'" 116 | return &$strategy $a $b 117 | } 118 | 119 | ## 120 | # exported functions 121 | # 122 | 123 | <# 124 | .SYNOPSIS 125 | Merges all the input objects using the specified conflict resolution strategy 126 | .OUTPUTS 127 | The merged value 128 | #> 129 | function Merge-Object { 130 | Param( 131 | # The objects to merge 132 | [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Named")] 133 | [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = "Explicit")] 134 | [ValidateNotNullOrEmpty()] 135 | [object[]] $Object, 136 | # The conflict resolution strategy 137 | [Parameter(Mandatory, ParameterSetName = "Named")] 138 | [ValidateNotNullOrEmpty()] 139 | [MergeStrategy] $Strategy, 140 | # Resolves merge conflicts between two objects 141 | [Parameter(Mandatory, ParameterSetName = "Explicit")] 142 | [ValidateScript( { $_.Ast.ParamBlock.Parameters.Count -in (0, 2) } )] 143 | [scriptblock] $Resolver 144 | ) 145 | 146 | if (-not $Resolver) { 147 | $Resolver = $Strategies["$Strategy"] 148 | } 149 | 150 | # we need to use explicit params because implicit params are invoked in a closure, 151 | # whereas we need our scriptblock to have access to $Strategies 152 | $reducer = { Param($a, $b); recursiveMerge $a $b $Resolver } 153 | $input | Reduce-Object $reducer 154 | } 155 | 156 | function Merge-ScriptBlock { 157 | [OutputType([scriptblock])] 158 | Param( 159 | [Parameter(Mandatory, ValueFromPipeline)] 160 | [ValidateScript( { $_ | % { $_.Ast.ParamBlock.Parameters.Count -eq 1 } | Test-All } )] 161 | [scriptblock[]] $ScriptBlock 162 | ) 163 | 164 | $reducer = { Param($a, $b) { Param($arg) &$a (&$b $arg) }.GetNewClosure() } 165 | $input | Reduce-Object $reducer 166 | } 167 | 168 | <# 169 | .SYNOPSIS 170 | Reduces a pipeline with the given reducer function 171 | .OUTPUTS 172 | The accumulated value 173 | .NOTES 174 | Reduce is an unapproved Verb, but none of the approved verbs accurately describe what we're doing, 175 | so we are conforming to Verb-Noun convention like other *-Object cmdlets instead 176 | #> 177 | function Reduce-Object { 178 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] 179 | Param( 180 | # The function applied to the accumulator and each element of the input 181 | [Parameter(Mandatory)] 182 | [ValidateScript( { $_.Ast.ParamBlock.Parameters.Count -in (0, 2) } )] 183 | [scriptblock] $Reducer, 184 | # The objects to merge 185 | [Parameter(Mandatory, ValueFromPipeline)] 186 | [ValidateNotNullOrEmpty()] 187 | [object[]] $Object, 188 | [ParamStyle] $ParamStyle = "Infer", 189 | [Direction] $Direction = "Right" 190 | ) 191 | 192 | # deduce and validate scriptblock invokation style 193 | $paramCount = $Reducer.Ast.ParamBlock.Parameters.Count 194 | $implicit = $ParamStyle -ne "Explicit" -and $paramCount -eq 0 195 | $explicit = $ParamStyle -ne "Implicit" -and $paramCount -eq 2 196 | if (-not ($implicit -or $explicit)) { 197 | throw "Could not reconcile Reducer parameter count '$paramCount' with param declaration style '$ParamStyle'" 198 | } 199 | 200 | # normalize input 201 | if ($Direction -eq [Direction]::Left) { 202 | [array]::Reverse($input) 203 | } 204 | 205 | # invoke the reducer 206 | $accum = $input | Select -First 1 207 | if ($implicit) { 208 | foreach ($object in $input | Select -Skip 1) { 209 | # invoke in scriptblock to minimize exposure of local variables 210 | $safelyScoped = { 211 | Param($a, $b, [scriptblock]$reducer) 212 | . $reducer.GetNewClosure() 213 | } 214 | $accum = &$safelyScoped $accum $object $Reducer 215 | } 216 | } 217 | if ($explicit) { 218 | foreach ($object in $input | Select -Skip 1) { 219 | $accum = &$Reducer $accum $object 220 | } 221 | } 222 | return $accum 223 | } 224 | 225 | <# 226 | .SYNOPSIS 227 | Returns true if all elements in the pipeline are truthy 228 | #> 229 | function Test-All { 230 | [OutputType([boolean])] 231 | Param() 232 | 233 | foreach ($e in $input) { 234 | if (-not $e) { 235 | return $false 236 | } 237 | } 238 | return $true 239 | } 240 | 241 | <# 242 | .SYNOPSIS 243 | Returns true if any of the elements in the pipeline are truthy 244 | #> 245 | function Test-Any { 246 | [OutputType([boolean])] 247 | Param() 248 | 249 | foreach ($e in $input) { 250 | if ($e) { 251 | return $true 252 | } 253 | } 254 | return $false 255 | } 256 | 257 | <# 258 | .SYNOPSIS 259 | Returns true if all elements in the pipeline are equival 260 | .DESCRIPTION 261 | Compares each element in the pipeline to the first pipeline element using a 262 | deep/recursive equality check of the properties and items 263 | #> 264 | function Test-Equality { 265 | [OutputType([boolean])] 266 | Param( 267 | # The objects to compare for equality 268 | [Parameter(Mandatory, ValueFromPipeline)] 269 | [ValidateNotNullOrEmpty()] 270 | [object[]] $Object 271 | ) 272 | 273 | $head = $input | Select -First 1 274 | $input | Select -Skip 1 | % { recursiveEquality $head $_ } | Test-All 275 | } 276 | 277 | ## 278 | # aliases 279 | # 280 | New-Alias -Name "merge" -Value Merge-Object 281 | New-Alias -Name "reduce" -Value Reduce-Object 282 | New-Alias -Name "forall" -Value Test-All 283 | New-Alias -Name "exists" -Value Test-Any 284 | -------------------------------------------------------------------------------- /functional.tests.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module $PSScriptRoot -DisableNameChecking -Force 3 | 4 | Describe "Reduce-Object" { 5 | Context "Given various invalid parameter invokations" { 6 | It "Should throw for invalid ParamStyle" { 7 | { 1..10 | Reduce-Object { } -ParamStyle "Joe Estevez" } | Should -Throw 8 | } 9 | It "Should throw for implicit params and 'Explicit'" { 10 | { 1..10 | Reduce-Object { $a + $b } -ParamStyle "Explicit" } | Should -Throw 11 | } 12 | It "Should throw for explicit params and 'Implicit'" { 13 | { 1..10 | Reduce-Object { Param($a, $b); $a + $b } -ParamStyle "Implicit" } | Should -Throw 14 | } 15 | } 16 | Context "Given various valid parameter invokations" { 17 | It "Should not throw for implicit params and 'Implicit'" { 18 | { 1..10 | Reduce-Object { $a + $b } -ParamStyle "Implicit" } | Should -Not -Throw 19 | } 20 | It "Should not throw for explicit params and 'Explicit'" { 21 | { 1..10 | Reduce-Object { Param($a, $b); $a + $b } -ParamStyle "Explicit" } | Should -Not -Throw 22 | } 23 | It "Should not throw for implicit params and 'Infer'" { 24 | { 1..10 | Reduce-Object { $a + $b } -ParamStyle "Infer" } | Should -Not -Throw 25 | } 26 | It "Should not throw for explicit params and 'Infer'" { 27 | { 1..10 | Reduce-Object { Param($a, $b); $a + $b } -ParamStyle "Infer" } | Should -Not -Throw 28 | } 29 | } 30 | Context "Given an implicit sum reducer" { 31 | It "Should sum up the numbers" { 32 | $values = 1..10 33 | $reducer = { $a + $b } 34 | $reduced = $values | Reduce-Object $reducer 35 | $measured = $values | Measure-Object -Sum | % Sum 36 | $reduced | Should -Be $measured 37 | } 38 | It "Should not have access to the parent scope" { 39 | $values = 1..10 40 | $c = 42 41 | $reducer = { $c } 42 | $reduced = $values | Reduce-Object $reducer 43 | $reduced | Should -Be $null 44 | } 45 | } 46 | Context "Given an explicit sum reducer" { 47 | It "Should sum up the numbers" { 48 | $values = 1..10 49 | $reducer = { Param($a, $b); $a + $b } 50 | $reduced = $values | Reduce-Object $reducer 51 | $measured = $values | Measure-Object -Sum | % Sum 52 | $reduced | Should -Be $measured 53 | } 54 | } 55 | } 56 | 57 | Describe "Merge-Object" { 58 | Context "Given an 'Override' merge strategy" { 59 | It "Should merge arrays without duplicates" { 60 | $merged = (1..5), (6..10) | Merge-Object -Strategy Override 61 | $merged | Should -BeOfType [int] 62 | $merged | Should -HaveCount 10 63 | } 64 | It "Should merge arrays with duplicates" { 65 | $merged = (1..7), (4..12) | Merge-Object -Strategy Override 66 | $merged | Should -BeOfType [int] 67 | $merged | Should -HaveCount 12 68 | } 69 | It "Should merge hashtables without duplicates" { 70 | $merged = @{ a = 1 }, @{ b = 2 } ` 71 | | Merge-Object -Strategy Override 72 | $merged | Should -BeOfType [hashtable] 73 | $merged.Keys | Should -HaveCount 2 74 | } 75 | It "Should merge hashtables with duplicates" { 76 | $merged = @{ a = 1; b = 2 }, @{ b = 3; c = 4 } ` 77 | | Merge-Object -Strategy Override 78 | $merged | Should -BeOfType [hashtable] 79 | $merged.Keys | Should -HaveCount 3 80 | $merged["b"] | Should -Be 3 81 | } 82 | It "Should merge objects without duplicates" { 83 | $merged = @{ a = 1 }, @{ b = 2 } ` 84 | | % { [PSCustomObject]$_ } ` 85 | | Merge-Object -Strategy Override 86 | $merged | Should -BeOfType [PSCustomObject] 87 | $merged.psobject.Properties | Should -HaveCount 2 88 | } 89 | It "Should merge objects with duplicates" { 90 | $merged = @{ a = 1; b = 2 }, @{ b = 3; c = 4 } ` 91 | | % { [PSCustomObject]$_ } ` 92 | | Merge-Object -Strategy Override 93 | $merged | Should -BeOfType [PSCustomObject] 94 | $merged.psobject.Properties | Should -HaveCount 3 95 | $merged.b | Should -Be 3 96 | } 97 | It "Should override inequal strings" { 98 | $merged = "joe", "estevez" | Merge-Object -Strategy Override 99 | $merged | Should -Be "estevez" 100 | } 101 | It "Should override inequal values" { 102 | $merged = "cat", 42 | Merge-Object -Strategy Override 103 | $merged | Should -Be 42 104 | } 105 | It "Should override equal values" { 106 | $merged = "cat", "cat" | Merge-Object -Strategy Override 107 | $merged | Should -Be "cat" 108 | } 109 | } 110 | Context "Given a 'Fail' merge strategy" { 111 | It "Should merge arrays without duplicates" { 112 | $merged = (1..5), (6..10) | Merge-Object -Strategy Fail 113 | $merged | Should -BeOfType [int] 114 | $merged | Should -HaveCount 10 115 | } 116 | It "Should merge arrays with duplicates" { 117 | $merged = (1..7), (4..12) | Merge-Object -Strategy Fail 118 | $merged | Should -BeOfType [int] 119 | $merged | Should -HaveCount 12 120 | } 121 | It "Should merge hashtables without duplicates" { 122 | $merged = @{ a = 1 }, @{ b = 2 } ` 123 | | Merge-Object -Strategy Fail 124 | $merged | Should -BeOfType [hashtable] 125 | $merged.Keys | Should -HaveCount 2 126 | } 127 | It "Should fail to merge hashtables with duplicates" { 128 | { 129 | @{ a = 1; b = 2 }, @{ b = 3; c = 4 } ` 130 | | Merge-Object -Strategy Fail 131 | } | Should -Throw 132 | } 133 | It "Should merge objects without duplicates" { 134 | $merged = @{ a = 1 }, @{ b = 2 } ` 135 | | % { [PSCustomObject]$_ } ` 136 | | Merge-Object -Strategy Fail 137 | $merged | Should -BeOfType [PSCustomObject] 138 | $merged.psobject.Properties | Should -HaveCount 2 139 | } 140 | It "Should fail to merge objects with duplicates" { 141 | { 142 | @{ a = 1; b = 2 }, @{ b = 3; c = 4 } ` 143 | | % { [PSCustomObject]$_ } ` 144 | | Merge-Object -Strategy Fail 145 | } | Should -Throw 146 | } 147 | It "Should fail to merge inequal strings" { 148 | { "joe", "estevez" | Merge-Object -Strategy Fail } ` 149 | | Should -Throw 150 | } 151 | It "Should fail to merge inequal values" { 152 | { "cat", 42 | Merge-Object -Strategy Fail } ` 153 | | Should -Throw 154 | } 155 | It "Should merge equal values" { 156 | $merged = "cat", "cat" | Merge-Object -Strategy Fail 157 | $merged | Should -Be "cat" 158 | } 159 | } 160 | Context "Given a resolver" { 161 | It "Should apply the resolver to irreconcilable types" { 162 | function resolver($a, $b) { 163 | $a + $b 164 | } 165 | $a = @{a = 1; b = 2 } 166 | $b = @{b = 3; d = 4 } 167 | $merged = ($a, $b) | Merge-Object -Resolver $Function:resolver 168 | $merged | Should -BeOfType [hashtable] 169 | $merged.Keys | Should -HaveCount 3 170 | $merged["b"] | Should -Be 5 171 | } 172 | } 173 | } 174 | 175 | Describe "Merge-ScriptBlock" { 176 | Context "Given valid input" { 177 | It "Should compose functions" { 178 | $fs = @( 179 | { Param($arg) "a" + $arg }, 180 | { Param($arg) "b" + $arg }, 181 | { Param($arg) "c" + $arg }, 182 | { Param($arg) "d" + $arg } 183 | ) 184 | $composed = $fs | Merge-ScriptBlock 185 | &$composed "e" | Should -Be "abcde" 186 | } 187 | } 188 | # # This is throwing a false negative: https://github.com/PowerShell/PowerShell/issues/9740 189 | # Context "Given invalid input" { 190 | # It "Should fail if one of the scriptblocks has invalid params" { 191 | # $fs = @( 192 | # { Param($arg) "a" + $arg }, 193 | # { Param($arg1, $arg2) "b" + $arg1 }, 194 | # { Param($arg) "c" + $arg }, 195 | # { Param($arg) "d" + $arg } 196 | # ) 197 | # { $fs | Merge-ScriptBlock } | Should -Throw 198 | # } 199 | # } 200 | } 201 | 202 | Describe "Test-All" { 203 | Context "Given valid input" { 204 | It "Should allow non-boolean values" { 205 | @(1, 3, "a", "chris", @{a = 3 }) | Test-All | Should -BeTrue 206 | } 207 | It "Should allow boolean values" { 208 | $true, $true, $true, $true, $true | Test-All | Should -BeTrue 209 | } 210 | } 211 | Context "Given invalid input" { 212 | It "Should allow non-boolean values" { 213 | 1, 3, "", "chris", @{a = 3 } | Test-All | Should -BeFalse 214 | } 215 | It "Should allow boolean values" { 216 | $true, $false, $true, $true, $true | Test-All | Should -BeFalse 217 | } 218 | } 219 | Context "Given single value" { 220 | It "Should pass on true" { 221 | $true | Test-All | Should -BeTrue 222 | } 223 | It "Should fail on false" { 224 | $false | Test-All | Should -BeFalse 225 | } 226 | } 227 | } 228 | 229 | Describe "Test-Any" { 230 | Context "Given valid input" { 231 | It "Should allow non-boolean values" { 232 | @(0, 1, 0, 0) | Test-Any | Should -BeTrue 233 | } 234 | It "Should allow boolean values" { 235 | $false, $true, $true, $false, $true | Test-Any | Should -BeTrue 236 | } 237 | } 238 | Context "Given invalid input" { 239 | It "Should allow non-boolean values" { 240 | @(0, 0, "", @(), 0) | Test-Any | Should -BeFalse 241 | } 242 | It "Should allow boolean values" { 243 | $false, $false, $false, $false, $false | Test-Any | Should -BeFalse 244 | } 245 | } 246 | Context "Given single value" { 247 | It "Should pass on true" { 248 | $true | Test-Any | Should -BeTrue 249 | } 250 | It "Should fail on false" { 251 | $false | Test-Any | Should -BeFalse 252 | } 253 | } 254 | } 255 | 256 | Describe "Test-Equality" { 257 | Context "Given leaves" { 258 | It "Should be true for equal values of the same type" { 259 | 3, 3 | Test-Equality | Should -BeTrue 260 | } 261 | It "Should be false for equal values of different types" { 262 | 3, "3" | Test-Equality | Should -BeFalse 263 | } 264 | } 265 | Context "Given arrays" { 266 | It "Should be false for deep inequal values" { 267 | @(1, 2, @{a = 1 }, 3), @(1, 2, @{a = 2 }, 3) | Test-Equality | Should -BeFalse 268 | } 269 | It "Should be true for deep equal values" { 270 | @(1, 2, @{a = 1 }, 3), @(1, 2, @{a = 1 }, 3) | Test-Equality | Should -BeTrue 271 | } 272 | } 273 | Context "Given hashtables" { 274 | It "Should be false for deep inequal values" { 275 | @{a = 1; b = @{c = 2 } }, @{a = 1; b = [pscustomobject]@{c = 2 } } | Test-Equality | Should -BeFalse 276 | } 277 | It "Should be true for deep equal values" { 278 | @{a = 1; b = @{c = 2 } }, @{a = 1; b = @{c = 2 } } | Test-Equality | Should -BeTrue 279 | } 280 | } 281 | Context "Given an array of PSCustomObject" { 282 | $a = @([PSCustomObject]@{ 'Name' = 'Foo'; 'Value' = 'Foo' }, [PSCustomObject]@{ 'Name' = 'Baz'; 'Value' = 'Baz' } ) 283 | $b = @([PSCustomObject]@{ 'Name' = 'xxx'; 'Value' = 'Foo' }, [PSCustomObject]@{ 'Name' = 'Baz'; 'Value' = 'Baz' } ) 284 | It "Should be false for deep inequal values" { 285 | $a, $b | Test-Equality | Should -BeFalse 286 | } 287 | It "Should be true for deep equal values" { 288 | $a, $a | Test-Equality | Should -BeTrue 289 | } 290 | } 291 | } 292 | --------------------------------------------------------------------------------