datum.psm1
#Region './Classes/1.DatumProvider.ps1' 0 class DatumProvider { hidden [bool]$IsDatumProvider = $true [hashtable]ToHashTable() { $result = ConvertTo-Datum -InputObject $this return $result } [System.Collections.Specialized.OrderedDictionary]ToOrderedHashTable() { $result = ConvertTo-Datum -InputObject $this return $result } } #EndRegion './Classes/1.DatumProvider.ps1' 17 #Region './Classes/FileProvider.ps1' 0 Class FileProvider : DatumProvider { hidden $Path hidden [hashtable] $Store hidden [hashtable] $DatumHierarchyDefinition hidden [hashtable] $StoreOptions hidden [hashtable] $DatumHandlers hidden [string] $Encoding FileProvider ($Path, $Store, $DatumHierarchyDefinition, $Encoding) { $this.Store = $Store $this.DatumHierarchyDefinition = $DatumHierarchyDefinition $this.StoreOptions = $Store.StoreOptions $this.Path = Get-Item $Path -ErrorAction SilentlyContinue $this.DatumHandlers = $DatumHierarchyDefinition.DatumHandlers $this.Encoding = $Encoding $Result = Get-ChildItem $path | ForEach-Object { if ($_.PSisContainer) { $val = [scriptblock]::Create("New-DatumFileProvider -Path `"$($_.FullName)`" -Store `$this.DataOptions -DatumHierarchyDefinition `$this.DatumHierarchyDefinition -Encoding `$this.Encoding") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } else { $val = [scriptblock]::Create("Get-FileProviderData -Path `"$($_.FullName)`" -DatumHandlers `$this.DatumHandlers -Encoding `$this.Encoding") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } } } } #EndRegion './Classes/FileProvider.ps1' 33 #Region './Classes/Node.ps1' 0 Class Node : hashtable { Node([hashtable]$NodeData) { $NodeData.keys | % { $This[$_] = $NodeData[$_] } $this | Add-Member -MemberType ScriptProperty -Name Roles -Value { $PathArray = $ExecutionContext.InvokeCommand.InvokeScript('Get-PSCallStack')[2].Position.text -split '\.' $PropertyPath = $PathArray[2..($PathArray.count - 1)] -join '\' Write-Warning "Resolve $PropertyPath" $obj = [PSCustomObject]@{} $currentNode = $obj if ($PathArray.Count -gt 3) { foreach ($property in $PathArray[2..($PathArray.count - 2)]) { Write-Debug "Adding $Property property" $currentNode | Add-Member -MemberType NoteProperty -Name $property -Value ([PSCustomObject]@{}) $currentNode = $currentNode.$property } } Write-Debug "Adding Resolved property to last object's property $($PathArray[-1])" $currentNode | Add-Member -MemberType NoteProperty -Name $PathArray[-1] -Value ($PropertyPath) return $obj } } static ResolveDscProperty($Path) { "Resolve-DscProperty $Path" } } #EndRegion './Classes/Node.ps1' 36 #Region './Private/Compare-Hashtable.ps1' 0 function Compare-Hashtable { [CmdletBinding()] Param( $ReferenceHashtable, $DifferenceHashtable, [string[]] $Property = ($ReferenceHashtable.Keys + $DifferenceHashtable.Keys | Select-Object -Unique) ) Write-Debug "Compare-Hashtable -Ref @{$($ReferenceHashtable.keys -join ';')} -Diff @{$($DifferenceHashtable.keys -join ';')} -Property [$($Property -join ', ')]" #Write-Debug "REF:`r`n$($ReferenceHashtable|ConvertTo-JSON)" #Write-Debug "DIFF:`r`n$($DifferenceHashtable|ConvertTo-JSON)" foreach ($PropertyName in $Property) { Write-Debug " Testing <$PropertyName>'s value" if ( ($inRef = $ReferenceHashtable.Contains($PropertyName)) -and ($inDiff = $DifferenceHashtable.Contains($PropertyName)) ) { if ($ReferenceHashtable[$PropertyName] -as [hashtable[]] -or $DifferenceHashtable[$PropertyName] -as [hashtable[]] ) { if ( (Compare-Hashtable -ReferenceHashtable $ReferenceHashtable[$PropertyName] -DifferenceHashtable $DifferenceHashtable[$PropertyName]) ) { Write-Debug " Skipping $PropertyName...." # If Compae returns something, they're not the same Continue } } else { Write-Debug "Comparing: $($ReferenceHashtable[$PropertyName]) With $($DifferenceHashtable[$PropertyName])" if ($ReferenceHashtable[$PropertyName] -ne $DifferenceHashtable[$PropertyName]) { [PSCustomObject]@{ SideIndicator = '<=' PropertyName = $PropertyName Value = $ReferenceHashtable[$PropertyName] } [PSCustomObject]@{ SideIndicator = '=>' PropertyName = $PropertyName Value = $DifferenceHashtable[$PropertyName] } } } } else { Write-Debug " Property $PropertyName Not in one Side: Ref: [$($ReferenceHashtable.Keys -join ',')] | [$($DifferenceHashtable.Keys -join ',')]" if ($inRef) { Write-Debug "$PropertyName found in Reference hashtable" [PSCustomObject]@{ SideIndicator = '<=' PropertyName = $PropertyName Value = $ReferenceHashtable[$PropertyName] } } else { Write-Debug "$PropertyName found in Difference hashtable" [PSCustomObject]@{ SideIndicator = '=>' PropertyName = $PropertyName Value = $DifferenceHashtable[$PropertyName] } } } } } #EndRegion './Private/Compare-Hashtable.ps1' 78 #Region './Private/ConvertTo-Datum.ps1' 0 function ConvertTo-Datum { param ( [Parameter(ValueFromPipeline)] $InputObject, [AllowNull()] $DatumHandlers = @{} ) begin { $HandlerNames = $DatumHandlers.Keys } process { if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { $hashKeys = [string[]]$InputObject.Keys foreach ($Key in $hashKeys) { $InputObject[$Key] = ConvertTo-Datum -InputObject $InputObject[$Key] -DatumHandlers $DatumHandlers } # Making the Ordered Dict Case Insensitive ([ordered]@{} + $InputObject) } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Datum -InputObject $object -DatumHandlers $DatumHandlers } ) , $collection } elseif (($InputObject -is [psobject] -or $InputObject -is [DatumProvider]) -and $InputObject -isnot [pscredential]) { $hash = [ordered]@{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Datum -InputObject $property.Value -DatumHandlers $DatumHandlers } $hash } # if There's a matching filter, process associated command and return result elseif ($HandlerNames -and ($result = & { foreach ($Handler in $HandlerNames) { $FilterModule, $FilterName = $Handler -split '::' if (!(Get-Module $FilterModule)) { Import-Module $FilterModule -Force -ErrorAction Stop } $FilterCommand = Get-Command -ErrorAction SilentlyContinue ('{0}\Test-{1}Filter' -f $FilterModule, $FilterName) if ($FilterCommand -and ($InputObject | &$FilterCommand)) { try { if ($ActionCommand = Get-Command -ErrorAction SilentlyContinue ('{0}\Invoke-{1}Action' -f $FilterModule, $FilterName)) { $ActionParams = @{} $CommandOptions = $Datumhandlers.$handler.CommandOptions.Keys # Populate the Command's params with what's in the Datum.yml, or from variables $Variables = Get-Variable foreach ( $ParamName in $ActionCommand.Parameters.keys ) { if ( $ParamName -in $CommandOptions ) { $ActionParams.add($ParamName, $Datumhandlers.$handler.CommandOptions[$ParamName]) } elseif ($Var = $Variables.Where{ $_.Name -eq $ParamName }) { $ActionParams.Add($ParamName, $Var.Value) } } $result = (&$ActionCommand @ActionParams) $result } } catch { Write-Warning "Error using Datum Handler $Handler, returning Input Object. The error was: '$($_.Exception.Message)'." $InputObject } } } })) { $result } else { $InputObject } } } #EndRegion './Private/ConvertTo-Datum.ps1' 107 #Region './Private/Get-DatumType.ps1' 0 function Get-DatumType { param ( [object] $DatumObject ) if ($DatumObject -is [hashtable] -or $DatumObject -is [System.Collections.Specialized.OrderedDictionary]) { 'hashtable' } elseif ($DatumObject -isnot [string] -and $DatumObject -is [System.Collections.IEnumerable]) { if ($Datumobject -as [hashtable[]]) { 'hash_array' } else { 'baseType_array' } } else { 'baseType' } } #EndRegion './Private/Get-DatumType.ps1' 30 #Region './Private/Get-MergeStrategyFromString.ps1' 0 function Get-MergeStrategyFromString { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $MergeStrategy ) <# MergeStrategy: MostSpecific merge_hash: MostSpecific merge_baseType_array: MostSpecific merge_hash_array: MostSpecific MergeStrategy: hash merge_hash: hash merge_baseType_array: MostSpecific merge_hash_array: MostSpecific merge_options: knockout_prefix: -- MergeStrategy: Deep merge_hash: deep merge_baseType_array: Unique merge_hash_array: DeepTuple merge_options: knockout_prefix: -- Tuple_Keys: - Name - Version #> Write-Debug -Message "Get-MergeStrategyFromString -MergeStrategy <$MergeStrategy>" switch -regex ($MergeStrategy) { '^First$|^MostSpecific$' { @{ merge_hash = 'MostSpecific' merge_baseType_array = 'MostSpecific' merge_hash_array = 'MostSpecific' } } '^hash$|^MergeTopKeys$' { @{ merge_hash = 'hash' merge_baseType_array = 'MostSpecific' merge_hash_array = 'MostSpecific' merge_options = @{ knockout_prefix = '--' } } } '^deep$|^MergeRecursively$' { @{ merge_hash = 'deep' merge_baseType_array = 'Unique' merge_hash_array = 'DeepTuple' merge_options = @{ knockout_prefix = '--' tuple_keys = @( 'Name', 'Version' ) } } } default { Write-Debug -Message "Couldn't Match the strategy $MergeStrategy" @{ merge_hash = 'MostSpecific' merge_baseType_array = 'MostSpecific' merge_hash_array = 'MostSpecific' } } } } #EndRegion './Private/Get-MergeStrategyFromString.ps1' 86 #Region './Private/Merge-DatumArray.ps1' 0 function Merge-DatumArray { [CmdletBinding()] Param( $ReferenceArray, $DifferenceArray, $Strategy = @{ }, $ChildStrategies = @{'^.*' = $Strategy }, $StartingPath ) Write-Debug "`tMerge-DatumArray -StartingPath <$StartingPath>" $knockout_prefix = [regex]::Escape($Strategy.merge_options.knockout_prefix).insert(0, '^') $HashArrayStrategy = $Strategy.merge_hash_array Write-Debug "`t`tHash Array Strategy: $HashArrayStrategy" $MergeBasetypeArraysStrategy = $Strategy.merge_basetype_array $MergedArray = [System.Collections.ArrayList]::new() $SortParams = @{} if ($PropertyNames = [String[]]$Strategy.merge_options.tuple_keys) { $SortParams.Add('Property', $PropertyNames) } if ($ReferenceArray -as [hashtable[]]) { Write-Debug "`t`tMERGING Array of Hashtables" if (!$HashArrayStrategy -or $HashArrayStrategy -match 'MostSpecific') { Write-Debug "`t`tMerge_hash_arrays Disabled. value: $HashArrayStrategy" $MergedArray = $ReferenceArray if ($Strategy.sort_merged_arrays) { $MergedArray = $MergedArray | Sort-Object @SortParams } return $MergedArray } switch -Regex ($HashArrayStrategy) { '^Sum|^Add' { (@($DifferenceArray) + @($ReferenceArray)) | ForEach-Object { $null = $MergedArray.add(([ordered]@{} + $_)) } } # MergeHashesByProperties '^Deep|^Merge' { Write-Debug "`t`t`tStrategy for Array Items: Merge Hash By tuple`r`n" # look at each $RefItems in $RefArray # if no PropertyNames defined, use all Properties of $RefItem # else use defined propertyNames # Search for DiffItem that has the same Property/Value pairs # if found, Merge-Datum (or MergeHashtable?) # if not found, add $DiffItem to $RefArray # look at each $RefItems in $RefArray $UsedDiffItems = [System.Collections.ArrayList]::new() foreach ($ReferenceItem in $ReferenceArray) { $ReferenceItem = [ordered]@{} + $ReferenceItem Write-Debug "`t`t`t .. Working on Merged Element $($MergedArray.Count)`r`n" # if no PropertyNames defined, use all Properties of $RefItem if (!$PropertyNames) { Write-Debug "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $PropertyNames = $ReferenceItem.Keys } $MergedItem = @{} + $ReferenceItem $DiffItemsToMerge = $DifferenceArray.Where{ $DifferenceItem = [ordered]@{} + $_ # Search for DiffItem that has the same Property/Value pairs than RefItem $CompareHashParams = @{ ReferenceHashtable = [ordered]@{} + $ReferenceItem DifferenceHashtable = $DifferenceItem Property = $PropertyNames } (!(Compare-Hashtable @CompareHashParams)) } Write-Debug "`t`t`t ..Items to merge: $($DiffItemsToMerge.Count)" $DiffItemsToMerge | ForEach-Object { $MergeItemsParams = @{ ParentPath = $StartingPath Strategy = $Strategy ReferenceHashtable = $MergedItem DifferenceHashtable = $_ ChildStrategies = $ChildStrategies } $MergedItem = Merge-Hashtable @MergeItemsParams } # If a diff Item has been used, save it to find the unused ones $null = $UsedDiffItems.AddRange($DiffItemsToMerge) $null = $MergedArray.Add($MergedItem) } $UnMergedItems = $DifferenceArray | ForEach-Object { if (!$UsedDiffItems.Contains($_)) { ([ordered]@{} + $_) } } if ($null -ne $UnMergedItems) { if ($UnMergedItems -is [System.Array]) { $null = $MergedArray.AddRange($UnMergedItems) } else { $null = $MergedArray.Add($UnMergedItems) } } } # UniqueByProperties '^Unique' { Write-Debug "`t`t`tSelecting Unique Hashes accross both arrays based on Property tuples" # look at each $DiffItems in $DiffArray # if no PropertyNames defined, use all Properties of $DiffItem # else use defined PropertyNames # Search for a RefItem that has the same Property/Value pairs # if Nothing is found # add current DiffItem to RefArray if (!$PropertyNames) { Write-Debug "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $PropertyNames = $ReferenceItem.Keys } $MergedArray = [System.Collections.ArrayList]::new() $ReferenceArray | ForEach-Object { $CurrentRefItem = $_ if (!( $MergedArray.Where{ !(Compare-Hashtable -Property $PropertyNames -ReferenceHashtable $CurrentRefItem -DifferenceHashtable $_ ) })) { $null = $MergedArray.Add(([ordered]@{} + $_)) } } $DifferenceArray | ForEach-Object { $CurrentDiffItem = $_ if (!( $MergedArray.Where{ !(Compare-Hashtable -Property $PropertyNames -ReferenceHashtable $CurrentDiffItem -DifferenceHashtable $_ ) })) { $null = $MergedArray.Add(([ordered]@{} + $_)) } } } } } $MergedArray } #EndRegion './Private/Merge-DatumArray.ps1' 159 #Region './Private/Merge-Hashtable.ps1' 0 function Merge-Hashtable { [outputType([hashtable])] [cmdletBinding()] Param( # [hashtable] These should stay ordered $ReferenceHashtable, # [hashtable] These should stay ordered $DifferenceHashtable, $Strategy = @{ merge_hash = 'hash' merge_baseType_array = 'MostSpecific' merge_hash_array = 'MostSpecific' merge_options = @{ knockout_prefix = '--' } }, $ChildStrategies = @{}, [string] $ParentPath ) Write-Debug "`tMerge-Hashtable -ParentPath <$ParentPath>" # Removing Case Sensitivity while keeping ordering $ReferenceHashtable = [ordered]@{} + $ReferenceHashtable $DifferenceHashtable = [ordered]@{} + $DifferenceHashtable $clonedReference = [ordered]@{} + $ReferenceHashtable if ($Strategy.merge_options.knockout_prefix) { $KnockoutPrefix = $Strategy.merge_options.knockout_prefix $KnockoutPrefixMatcher = [regex]::escape($KnockoutPrefix).insert(0, '^') } else { $KnockoutPrefixMatcher = [regex]::escape('--').insert(0, '^') } Write-Debug "`t Knockout Prefix Matcher: $knockoutPrefixMatcher" $knockedOutKeys = $ReferenceHashtable.keys.where{ $_ -match $KnockoutPrefixMatcher }.foreach{ $_ -replace $KnockoutPrefixMatcher } Write-Debug "`t Knockedout Keys: [$($knockedOutKeys -join ', ')] from reference Hashtable Keys [$($ReferenceHashtable.keys -join ', ')]" foreach ($currentKey in $DifferenceHashtable.keys) { Write-Debug "`t CurrentKey: $currentKey" if ($currentKey -in $knockedOutKeys) { Write-Debug "`t`tThe Key $currentkey is knocked out from the reference Hashtable." } elseif ($currentKey -match $KnockoutPrefixMatcher -and !$ReferenceHashtable.contains(($currentKey -replace $KnockoutPrefixMatcher))) { # it's a knockout coming from a lower level key, it should only apply down from here Write-Debug "`t`tKnockout prefix found for $currentKey in Difference hashtable, and key not set in Reference hashtable" if (!$ReferenceHashtable.contains($currentKey)) { Write-Debug "`t`t..adding knockout prefixed key for $curretKey to block further merges" $clonedReference.add($currentKey, $null) } } elseif (!$ReferenceHashtable.contains($currentKey) ) { #if the key does not exist in reference ht, create it using the DiffHt's value Write-Debug "`t Added Missing Key $currentKey of value: $($DifferenceHashtable[$currentKey]) from difference HT" $clonedReference.add($currentKey, $DifferenceHashtable[$currentKey]) } else { #the key exists, and it's not a knockout entry $RefHashItemValueType = Get-DatumType $ReferenceHashtable[$currentKey] $DiffHashItemValueType = Get-DatumType $DifferenceHashtable[$currentKey] Write-Debug "for Key $currentKey REF:[$RefHashItemValueType] | DIFF:[$DiffHashItemValueType]" if ($ParentPath) { $ChildPath = (Join-Path $ParentPath $currentKey) } else { $ChildPath = $currentKey } switch ($RefHashItemValueType) { 'hashtable' { if ($Strategy.merge_hash -eq 'deep') { Write-Debug "`t`t .. Merging Datums at current path $ChildPath" # if there's no Merge override for the subkey's path in the (not subkeys), # merge HASHTABLE with same strategy # otherwise, merge Datum $ChildStrategy = Get-MergeStrategyFromPath -Strategies $ChildStrategies -PropertyPath $ChildPath if ($ChildStrategy.Default) { Write-Debug "`t`t ..Merging using the current Deep Strategy, Bypassing default" $MergePerDefault = @{ ParentPath = $ChildPath Strategy = $Strategy ReferenceHashtable = $ReferenceHashtable[$currentKey] DifferenceHashtable = $DifferenceHashtable[$currentKey] ChildStrategies = $ChildStrategies } $subMerge = Merge-Hashtable @MergePerDefault } else { Write-Debug "`t`t ..Merging using Override Strategy $($ChildStrategy|ConvertTo-Json)" $MergeDatumParam = @{ StartingPath = $ChildPath ReferenceDatum = $ReferenceHashtable[$currentKey] DifferenceDatum = $DifferenceHashtable[$currentKey] Strategies = $ChildStrategies } $subMerge = Merge-Datum @MergeDatumParam } Write-Debug "`t # Submerge $($submerge|ConvertTo-Json)." $clonedReference[$currentKey] = $subMerge } } 'baseType' { #do nothing to use most specific value (quicker than default) } # Default used for hash_array, baseType_array Default { Write-Debug "`t .. Merging Datums at current path $ChildPath`r`n$($Strategy|ConvertTo-Json)" $MergeDatumParams = @{ StartingPath = $ChildPath Strategies = $ChildStrategies ReferenceDatum = $ReferenceHashtable[$currentKey] DifferenceDatum = $DifferenceHashtable[$currentKey] } if ($clonedReference.$currentKey -is [System.Array]) { [System.Array]$clonedReference[$currentKey] = Merge-Datum @MergeDatumParams } else { $clonedReference[$currentKey] = Merge-Datum @MergeDatumParams } Write-Debug "`t .. Datum Merged for path $ChildPath" } } } } return $clonedReference } #EndRegion './Private/Merge-Hashtable.ps1' 157 #Region './Public/Get-DatumRSOP.ps1' 0 function Get-DatumRsop { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $Datum, [Parameter(Mandatory = $true)] [hashtable[]] $AllNodes, [Parameter()] [string] $CompositionKey = 'Configurations', [Parameter()] [scriptblock] $Filter = {} ) if ($Filter.ToString() -ne ([System.Management.Automation.ScriptBlock]::Create( {})).ToString()) { Write-Verbose -Message "Filter: $($Filter.ToString())" $AllNodes = [System.Collections.Hashtable[]]$allNodes.Where($Filter) Write-Verbose -Message "Node count after applying filter: $($AllNodes.Count)" } foreach ($node in $AllNodes) { $rsopNode = $node.clone() $configurations = Lookup $CompositionKey -Node $node -DatumTree $Datum -DefaultValue @() if ($rsopNode.contains($CompositionKey)) { $rsopNode[$CompositionKey] = $configurations } else { $rsopNode.Add($CompositionKey, $configurations) } $configurations.Foreach{ if (-not $rsopNode.Contains($_)) { $rsopNode.Add($_, (Lookup -PropertyPath $_ -DefaultValue @{} -Node $node -DatumTree $Datum)) } else { $rsopNode[$_] = Lookup -PropertyPath $_ -DefaultValue @{} -Node $node -DatumTree $Datum } } $rsopNode } } #EndRegion './Public/Get-DatumRSOP.ps1' 57 #Region './Public/Get-FileProviderData.ps1' 0 function Get-FileProviderData { [OutputType([System.Array])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [AllowNull()] [hashtable] $DatumHandlers = @{}, [Parameter()] [ValidateSet('Ascii', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] [string] $Encoding = 'Default' ) if (-not $script:FileProviderDataCache) { $script:FileProviderDataCache = @{} } $file = Get-Item -Path $Path if ($script:FileProviderDataCache.ContainsKey($file.FullName) -and $file.LastWriteTime -eq $script:FileProviderDataCache[$file.FullName].Metadata.LastWriteTime) { Write-Verbose -Message "Getting File Provider Cache for Path: $Path" , $script:FileProviderDataCache[$file.FullName].Value } else { Write-Verbose -Message "Getting File Provider Data for Path: $Path" $data = switch ($file.Extension) { '.psd1' { Import-PowerShellDataFile -Path $file | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.json' { ConvertFrom-Json -InputObject (Get-Content -Path $Path -Encoding $Encoding -Raw) | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.yml' { ConvertFrom-Yaml -Yaml (Get-Content -Path $Path -Encoding $Encoding -Raw) -Ordered | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.yaml' { ConvertFrom-Yaml -Yaml (Get-Content -Path $Path -Encoding $Encoding -Raw) -Ordered | ConvertTo-Datum -DatumHandlers $DatumHandlers } Default { Write-Verbose -Message "File extension $($file.Extension) not supported. Defaulting on RAW." Get-Content -Path $Path -Encoding $Encoding -Raw } } $script:FileProviderDataCache[$file.FullName] = @{ Metadata = $file Value = $data } , $data } } #EndRegion './Public/Get-FileProviderData.ps1' 68 #Region './Public/Get-MergeStrategyFromPath.ps1' 0 function Get-MergeStrategyFromPath { [CmdletBinding()] Param( $Strategies, $PropertyPath ) Write-Debug "`tGet-MergeStrategyFromPath -PropertyPath <$PropertyPath> -Strategies [$($Strategies.keys -join ', ')], count $($Strategies.count)" # Select Relevant strategy # Use exact path match first # or try Regex in order if ($Strategies.($PropertyPath)) { $StrategyKey = $PropertyPath Write-Debug "`t Strategy found for exact key $StrategyKey" } elseif ($Strategies.keys -and ($StrategyKey = [string]($Strategies.keys.where{ $_.StartsWith('^') -and $_ -as [regex] -and $PropertyPath -match $_ } | Select-Object -First 1)) ) { Write-Debug "`t Strategy matching regex $StrategyKey" } else { Write-Debug "`t No Strategy found" return } Write-Debug "`t StrategyKey: $StrategyKey" if ( $Strategies[$StrategyKey] -is [string]) { Write-Debug "`t Returning Strategy $StrategyKey from String '$($Strategies[$StrategyKey])'" Get-MergeStrategyFromString $Strategies[$StrategyKey] } else { Write-Debug "`t Returning Strategy $StrategyKey of type '$($Strategies[$StrategyKey].Strategy)'" $Strategies[$StrategyKey] } } #EndRegion './Public/Get-MergeStrategyFromPath.ps1' 42 #Region './Public/Invoke-TestHandlerAction.ps1' 0 function Invoke-TestHandlerAction { [OutputType([string])] [CmdletBinding()] param ( [Parameter()] [string] $Password, [Parameter()] [object] $Test, [Parameter()] [object] $Datum ) @" Action: $handler Node: $($Node|fl *|Out-String) Params: $($PSBoundParameters | ConvertTo-Json) "@ } #EndRegion './Public/Invoke-TestHandlerAction.ps1' 27 #Region './Public/Merge-Datum.ps1' 0 function Merge-Datum { [OutputType([System.Array])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $StartingPath, [Parameter(Mandatory = $true)] [object] $ReferenceDatum, [Parameter(Mandatory = $true)] [AllowNull()] [object] $DifferenceDatum, [Parameter()] [hashtable] $Strategies = @{ '^.*' = 'MostSpecific' } ) Write-Debug -Message "Merge-Datum -StartingPath <$StartingPath>" $strategy = Get-MergeStrategyFromPath -Strategies $Strategies -PropertyPath $startingPath -Verbose Write-Verbose -Message " Merge Strategy: @$($strategy | ConvertTo-Json)" $referenceDatumType = Get-DatumType -DatumObject $ReferenceDatum $differenceDatumType = Get-DatumType -DatumObject $DifferenceDatum if ($referenceDatumType -ne $differenceDatumType) { Write-Warning -Message "Cannot merge different types in path '$StartingPath' REF:[$referenceDatumType] | DIFF:[$differenceDatumType]$($DifferenceDatum.GetType()) , returning most specific Datum." return $ReferenceDatum } if ($strategy -is [string]) { $strategy = Get-MergeStrategyFromString -MergeStrategy $strategy } switch ($referenceDatumType) { 'BaseType' { return $ReferenceDatum } 'hashtable' { $mergeParams = @{ ReferenceHashtable = $ReferenceDatum DifferenceHashtable = $DifferenceDatum Strategy = $strategy ParentPath = $StartingPath ChildStrategies = $Strategies } if ($strategy.merge_hash -match '^MostSpecific$|^First') { return $ReferenceDatum } else { Merge-Hashtable @mergeParams } } 'baseType_array' { switch -Regex ($strategy.merge_baseType_array) { '^MostSpecific$|^First' { return $ReferenceDatum } '^Unique' { if ($regexPattern = $strategy.merge_options.knockout_prefix) { $regexPattern = $regexPattern.insert(0, '^') $result = @(($ReferenceDatum + $DifferenceDatum).Where{ $_ -notmatch $regexPattern } | Select-Object -Unique) , $result } else { $result = @(($ReferenceDatum + $DifferenceDatum) | Select-Object -Unique) , $result } } '^Sum|^Add' { #--> $ref + $diff -$kop if ($regexPattern = $strategy.merge_options.knockout_prefix) { $regexPattern = $regexPattern.insert(0, '^') , (($ReferenceDatum + $DifferenceDatum).Where{ $_ -notMatch $regexPattern }) } else { , ($ReferenceDatum + $DifferenceDatum) } } Default { return (, $ReferenceDatum) } } } 'hash_array' { $MergeDatumArrayParams = @{ ReferenceArray = $ReferenceDatum DifferenceArray = $DifferenceDatum Strategy = $strategy ChildStrategies = $Strategies StartingPath = $StartingPath } switch -Regex ($strategy.merge_hash_array) { '^MostSpecific|^First' { return $ReferenceDatum } '^UniqueKeyValTuples' { #--> $ref + $diff | ? % key in Tuple_Keys -> $ref[Key] -eq $diff[key] is not already int output , (Merge-DatumArray @MergeDatumArrayParams) } '^DeepTuple|^DeepItemMergeByTuples' { #--> $ref + $diff | ? % key in Tuple_Keys -> $ref[Key] -eq $diff[key] is merged up , (Merge-DatumArray @MergeDatumArrayParams) } '^Sum' { #--> $ref + $diff (@($DifferenceArray) + @($ReferenceArray)).Foreach{ $null = $MergedArray.Add(([ordered]@{} + $_)) } , $MergedArray } Default { return , $ReferenceDatum } } } } } #EndRegion './Public/Merge-Datum.ps1' 164 #Region './Public/New-DatumFileProvider.ps1' 0 function New-DatumFileProvider { [CmdletBinding()] param ( [Parameter()] [Alias('DataOptions')] [AllowNull()] [object] $Store, [Parameter()] [AllowNull()] [hashtable] $DatumHierarchyDefinition = @{}, [Parameter()] [string] $Path = $Store.StoreOptions.Path, [Parameter()] [ValidateSet('Ascii', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] [string] $Encoding = 'Default' ) if (-not $DatumHierarchyDefinition) { $DatumHierarchyDefinition = @{} } [FileProvider]::new($Path, $Store, $DatumHierarchyDefinition, $Encoding) } #EndRegion './Public/New-DatumFileProvider.ps1' 33 #Region './Public/New-DatumStructure.ps1' 0 function New-DatumStructure { [OutputType([hashtable])] [CmdletBinding(DefaultParameterSetName = 'FromConfigFile')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'DatumHierarchyDefinition')] [Alias('Structure')] [hashtable] $DatumHierarchyDefinition, [Parameter(Mandatory = $true, ParameterSetName = 'FromConfigFile')] [System.IO.FileInfo] $DefinitionFile, [Parameter()] [ValidateSet('Ascii', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] [string] $Encoding = 'Default' ) switch ($PSCmdlet.ParameterSetName) { 'DatumHierarchyDefinition' { if ($DatumHierarchyDefinition.Contains('DatumStructure')) { Write-Debug -Message 'Loading Datum from Parameter' } elseif ($DatumHierarchyDefinition.Path) { $datumHierarchyFolder = $DatumHierarchyDefinition.Path Write-Debug -Message "Loading default Datum from given path $datumHierarchyFolder" } else { Write-Warning -Message 'Desperate attempt to load Datum from Invocation origin...' $callStack = Get-PSCallStack $datumHierarchyFolder = $callStack[-1].PSScriptRoot Write-Warning -Message " ---> $datumHierarchyFolder" } } 'FromConfigFile' { if ((Test-Path -Path $DefinitionFile)) { $DefinitionFile = (Get-Item -Path $DefinitionFile -ErrorAction Stop) Write-Debug -Message "File $DefinitionFile found. Loading..." $DatumHierarchyDefinition = Get-FileProviderData -Path $DefinitionFile.FullName -Encoding $Encoding if (-not $DatumHierarchyDefinition.Contains('ResolutionPrecedence')) { throw 'Invalid Datum Hierarchy Definition' } $datumHierarchyFolder = $DefinitionFile.Directory.FullName Write-Debug -Message "Datum Hierachy Parent folder: $datumHierarchyFolder" } else { throw 'Datum Hierarchy Configuration not found' } } } $root = @{} if ($datumHierarchyFolder -and -not $DatumHierarchyDefinition.DatumStructure) { $structures = foreach ($store in (Get-ChildItem -Directory -Path $datumHierarchyFolder)) { @{ StoreName = $store.BaseName StoreProvider = 'Datum::File' StoreOptions = @{ Path = $store.FullName } } } if ($DatumHierarchyDefinition.Contains('DatumStructure')) { $DatumHierarchyDefinition['DatumStructure'] = $structures } else { $DatumHierarchyDefinition.Add('DatumStructure', $structures) } } # Define the default hierachy to be the StoreNames, when nothing is specified if ($datumHierarchyFolder -and -not $DatumHierarchyDefinition.ResolutionPrecedence) { if ($DatumHierarchyDefinition.Contains('ResolutionPrecedence')) { $DatumHierarchyDefinition['ResolutionPrecedence'] = $structures.StoreName } else { $DatumHierarchyDefinition.Add('ResolutionPrecedence', $structures.StoreName) } } # Adding the Datum Definition to Root object $root.Add('__Definition', $DatumHierarchyDefinition) foreach ($store in $DatumHierarchyDefinition.DatumStructure) { $storeParams = @{ Store = (ConvertTo-Datum ([hashtable]$store).clone()) Path = $store.StoreOptions.Path Encoding = $Encoding } # Accept Module Specification for Store Provider as String (unversioned) or Hashtable if ($store.StoreProvider -is [string]) { $storeProviderModule, $storeProviderName = $store.StoreProvider -split '::' } else { $storeProviderModule = $store.StoreProvider.ModuleName $storeProviderName = $store.StoreProvider.ProviderName if ($store.StoreProvider.ModuleVersion) { $storeProviderModule = @{ ModuleName = $storeProviderModule ModuleVersion = $store.StoreProvider.ModuleVersion } } } if (-not ($module = Get-Module -Name $storeProviderModule -ErrorAction SilentlyContinue)) { $module = Import-Module $storeProviderModule -Force -ErrorAction Stop -PassThru } $moduleName = ($module | Select-Object -First 1).Name $newProviderCmd = Get-Command ('{0}\New-Datum{1}Provider' -f $moduleName, $storeProviderName) if ($storeParams.Path -and -not [System.IO.Path]::IsPathRooted($storeParams.Path) -and $datumHierarchyFolder) { Write-Debug -Message 'Replacing Store Path with AbsolutePath' $storePath = Join-Path -Path $datumHierarchyFolder -ChildPath $storeParams.Path -Resolve -ErrorAction Stop $storeParams['Path'] = $storePath } if ($newProviderCmd.Parameters.Keys -contains 'DatumHierarchyDefinition') { Write-Debug -Message 'Adding DatumHierarchyDefinition to Store Params' $storeParams.Add('DatumHierarchyDefinition', $DatumHierarchyDefinition) } $storeObject = &$newProviderCmd @storeParams Write-Debug -Message "Adding key $($store.StoreName) to Datum root object" $root.Add($store.StoreName, $storeObject) } #return the Root Datum hashtable $root } #EndRegion './Public/New-DatumStructure.ps1' 159 #Region './Public/Resolve-Datum.ps1' 0 function Resolve-Datum { [OutputType([System.Array])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $PropertyPath, [Parameter(Position = 1)] [Alias('Node')] [object] $Variable = $ExecutionContext.InvokeCommand.InvokeScript('$Node'), [Parameter()] [string] $VariableName = 'Node', [Parameter()] [Alias('DatumStructure')] [object] $DatumTree = $ExecutionContext.InvokeCommand.InvokeScript('$ConfigurationData.Datum'), [Parameter(ParameterSetName = 'UseMergeOptions')] [Alias('SearchBehavior')] [hashtable] $Options, [Parameter()] [Alias('SearchPaths')] [string[]] $PathPrefixes = $DatumTree.__Definition.ResolutionPrecedence, [Parameter()] [int] $MaxDepth = $( if ($mxdDpth = $DatumTree.__Definition.default_lookup_options.MaxDepth) { $mxdDpth } else { -1 }) ) # Manage lookup options: <# default_lookup_options Lookup_options options (argument) Behaviour MostSpecific for ^.* Present default_lookup_options + most Specific if not ^.* Present lookup_options + Default to most Specific if not ^.* Present options + Default to Most Specific if not ^.* Present Present Lookup_options + Default for ^.* if !Exists Present Present options + Default for ^.* if !Exists Present Present options override lookup options + Most Specific if !Exists Present Present Present options override lookup options + default for ^.* +========================+================+====================+============================================================+ | default_lookup_options | Lookup_options | options (argument) | Behaviour | +========================+================+====================+============================================================+ | | | | MostSpecific for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | | | default_lookup_options + most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | Present | | lookup_options + Default to most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | | Present | options + Default to Most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | | Lookup_options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | | Present | options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | Present | Present | options override lookup options + Most Specific if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | Present | options override lookup options + default for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ If there's no default options, auto-add default options of mostSpecific merge, and tag as 'default' if there's a default options, use that strategy and tag as 'default' if the options implements ^.*, do not add Default_options, and do not tag 1. Defaults to Most Specific 2. Allow setting your own default, with precedence for non-default options 3. Overriding ^.* without tagging it as default (always match unless) #> Write-Debug -Message "Resolve-Datum -PropertyPath <$PropertyPath> -Node $($Node.Name)" # Make options an ordered case insensitive variable if ($Options) { $Options = [ordered]@{} + $Options } if (-not $DatumTree.__Definition.default_lookup_options) { $default_options = Get-MergeStrategyFromString Write-Verbose -Message ' Default option not found in Datum Tree' } else { if ($DatumTree.__Definition.default_lookup_options -is [string]) { $default_options = Get-MergeStrategyFromString -MergeStrategy $DatumTree.__Definition.default_lookup_options } else { $default_options = $DatumTree.__Definition.default_lookup_options } #TODO: Add default_option input validation Write-Verbose -Message " Found default options in Datum Tree of type $($default_options.Strategy)." } if ($DatumTree.__Definition.lookup_options) { Write-Debug -Message ' Lookup options found.' $lookup_options = @{} + $DatumTree.__Definition.lookup_options } else { $lookup_options = @{} } # Transform options from string to strategy hashtable foreach ($optKey in ([string[]]$lookup_options.Keys)) { if ($lookup_options[$optKey] -is [string]) { $lookup_options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $lookup_options[$optKey] } } foreach ($optKey in ([string[]]$Options.Keys)) { if ($Options[$optKey] -is [string]) { $Options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $Options[$optKey] } } # using options if specified or lookup_options otherwise if (-not $Options) { $Options = $lookup_options } # Add default strategy for ^.* if not present, at the end if (([string[]]$Options.Keys) -notcontains '^.*') { # Adding Default flag $default_options['Default'] = $true $Options.Add('^.*', $default_options) } # Create the variable to be used as Pivot in prefix path if ($Variable -and $VariableName) { Set-Variable -Name $VariableName -Value $Variable -Force } # Scriptblock in path detection patterns $pattern = '(?<opening><%=)(?<sb>.*?)(?<closure>%>)' $propertySeparator = [System.IO.Path]::DirectorySeparatorChar $splitPattern = [regex]::Escape($propertySeparator) $depth = 0 $mergeResult = $null # Get the strategy for this path, to be used for merging $startingMergeStrategy = Get-MergeStrategyFromPath -PropertyPath $PropertyPath -Strategies $Options # Walk every search path in listed order, and return datum when found at end of path foreach ($searchPrefix in $PathPrefixes) { #through the hierarchy $arraySb = [System.Collections.ArrayList]@() $currentSearch = Join-Path -Path $searchPrefix -ChildPath $PropertyPath Write-Verbose -Message '' Write-Verbose -Message " Lookup <$currentSearch> $($Node.Name)" #extract script block for execution into array, replace by substition strings {0},{1}... $newSearch = [regex]::Replace($currentSearch, $pattern, { param($match) $expr = $match.groups['sb'].value $index = $arraySb.Add($expr) "`$({$index})" }, @('IgnoreCase', 'SingleLine', 'MultiLine')) $pathStack = $newSearch -split $splitPattern # Get value for this property path $datumFound = Resolve-DatumPath -Node $Node -DatumTree $DatumTree -PathStack $pathStack -PathVariables $arraySb if ($datumFound -is [DatumProvider]) { $datumFound = $datumFound.ToOrderedHashTable() } Write-Debug -Message " Depth: $depth; Merge options = $($Options.count)" #Stop processing further path at first value in 'MostSpecific' mode (called 'first' in Puppet hiera) if ($null -ne $datumFound -and ($startingMergeStrategy.Strategy -match '^MostSpecific|^First')) { return $datumFound } elseif ($null -ne $datumFound) { if ($null -eq $mergeResult) { $mergeResult = $datumFound } else { $MergeParams = @{ StartingPath = $PropertyPath ReferenceDatum = $mergeResult DifferenceDatum = $datumFound Strategies = $Options } $mergeResult = Merge-Datum @MergeParams } } #if we've reached the Maximum Depth allowed, return current result and stop further execution if ($depth -eq $MaxDepth) { Write-Debug " Max depth of $MaxDepth reached. Stopping." , $mergeResult return } } , $mergeResult } #EndRegion './Public/Resolve-Datum.ps1' 236 #Region './Public/Resolve-DatumPath.ps1' 0 function Resolve-DatumPath { [OutputType([System.Array])] [CmdletBinding()] param ( [Parameter()] [Alias('Variable')] $Node, [Parameter()] [Alias('DatumStructure')] [object] $DatumTree, [Parameter()] [string[]] $PathStack, [Parameter()] [System.Collections.ArrayList] $PathVariables ) $currentNode = $DatumTree $propertySeparator = '.' #[System.IO.Path]::DirectorySeparatorChar $index = -1 Write-Debug -Message "`t`t`t" foreach ($stackItem in $PathStack) { $index++ $relativePath = $PathStack[0..$index] Write-Debug -Message "`t`t`tCurrent Path: `$Datum$propertySeparator$($relativePath -join $propertySeparator)" $remainingStack = $PathStack[$index..($PathStack.Count - 1)] Write-Debug -Message "`t`t`t`tbranch of path Left to walk: $propertySeparator$($remainingStack[1..$remainingStack.Length] -join $propertySeparator)" if ($stackItem -match '\{\d+\}') { Write-Debug -Message "`t`t`t`t`tReplacing expression $stackItem" $stackItem = [scriptblock]::Create(($stackItem -f ([string[]]$PathVariables)) ).Invoke() Write-Debug -Message ($stackItem | Format-List * | Out-String) $pathItem = $stackItem } else { $pathItem = $currentNode.($ExecutionContext.InvokeCommand.ExpandString($stackItem)) } # if $pathItem is $null, it won't have subkeys, stop execution for this Prefix if ($null -eq $pathItem) { Write-Verbose -Message " NULL FOUND at `$Datum.$($ExecutionContext.InvokeCommand.ExpandString(($relativePath -join $propertySeparator) -f [string[]]$PathVariables))`t`t <`$Datum$propertySeparator$(($relativePath -join $propertySeparator) -f [string[]]$PathVariables)>" if ($remainingStack.Count -gt 1) { Write-Verbose -Message "`t`t----> before: $propertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($remainingStack[1..($remainingStack.Count-1)] -join $propertySeparator)))`t`t <$(($remainingStack[1..($remainingStack.Count-1)] -join $propertySeparator) -f [string[]]$PathVariables)>" } return $null } else { $currentNode = $pathItem } if ($remainingStack.Count -eq 1) { Write-Verbose -Message " VALUE found at `$Datum$propertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($relativePath -join $propertySeparator) -f [string[]]$PathVariables))" , $currentNode } } } #EndRegion './Public/Resolve-DatumPath.ps1' 73 #Region './Public/Test-TestHandlerFilter.ps1' 0 function Test-TestHandlerFilter { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(ValueFromPipeline = $true)] [object]$InputObject ) $InputObject -is [string] -and $InputObject -match '^\[TEST=[\w\W]*\]$' } #EndRegion './Public/Test-TestHandlerFilter.ps1' 12 |