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 [string]$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 $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 | ForEach-Object {
            $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 -Message "Resolve $propertyPath"

            $obj = [PSCustomObject]@{}
            $currentNode = $obj
            if ($pathArray.Count -gt 3)
            {
                foreach ($property in $pathArray[2..($pathArray.count - 2)])
                {
                    Write-Debug -Message "Adding $Property property"
                    $currentNode | Add-Member -MemberType NoteProperty -Name $property -Value ([PSCustomObject]@{})
                    $currentNode = $currentNode.$property
                }
            }
            Write-Debug -Message "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 -DefaultValue $Path"
    }
}
#EndRegion './Classes/Node.ps1' 36
#Region './Private/Compare-Hashtable.ps1' 0
function Compare-Hashtable
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $ReferenceHashtable,

        [Parameter(Mandatory = $true)]
        [hashtable]
        $DifferenceHashtable,

        [Parameter()]
        [string[]]
        $Property = ($ReferenceHashtable.Keys + $DifferenceHashtable.Keys | Select-Object -Unique)
    )

    Write-Debug -Message "Compare-Hashtable -Ref @{$($ReferenceHashtable.keys -join ';')} -Diff @{$($DifferenceHashtable.keys -join ';')} -Property [$($Property -join ', ')]"
    #Write-Debug -Message "REF:`r`n$($ReferenceHashtable | ConvertTo-Json)"
    #Write-Debug -Message "DIFF:`r`n$($DifferenceHashtable | ConvertTo-Json)"

    foreach ($propertyName in $Property)
    {
        Write-Debug -Message " 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 -Message " Skipping $propertyName...."
                    # If Compae returns something, they're not the same
                    continue
                }
            }
            else
            {
                Write-Debug -Message "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 -Message " Property $propertyName Not in one Side: Ref: [$($ReferenceHashtable.Keys -join ',')] | [$($DifferenceHashtable.Keys -join ',')]"
            if ($inRef)
            {
                Write-Debug -Message "$propertyName found in Reference hashtable"
                [PSCustomObject]@{
                    SideIndicator = '<='
                    PropertyName  = $propertyName
                    Value         = $ReferenceHashtable[$propertyName]
                }
            }
            else
            {
                Write-Debug -Message "$propertyName found in Difference hashtable"
                [PSCustomObject]@{
                    SideIndicator = '=>'
                    PropertyName  = $propertyName
                    Value         = $DifferenceHashtable[$propertyName]
                }
            }
        }
    }

}
#EndRegion './Private/Compare-Hashtable.ps1' 81
#Region './Private/Copy-Object.ps1' 0
function Copy-Object
{
    <#
    .SYNOPSIS
        Creates a real copy of an object recursive including all the referenced objects it points to.

    .DESCRIPTION

        In .net reference types (classes), cannot be copied easily. If a type implements the IClonable interface it can be copied
        or cloned but the objects it references to will not be cloned. Rather the reference is cloned like shown in this example:

        $a = @{
            k1 = 'v1'
            k2 = @{
                kk1 = 'vv1'
                kk2 = 'vv2'
            }
        }

        $b = @{}
        $validKeys = 'k1', 'k2'
        foreach ($validKey in $validKeys)
        {
            if ($a.ContainsKey($validKey))
            {
                $b.Add($validKey, $a.Item($validKey))
            }
        }

        Write-Host '-------- Before removal of kk2 -------------'
        Write-Host "Key count of a.k2: $($a.k2.Keys.Count)"
        Write-Host "Key count in b.k2: $($b.k2.Keys.Count)"

        $b.k2.Remove('kk2')
        Write-Host '-------- After removal of kk2 --------------'
        Write-Host "Key count of a.k2: $($a.k2.Keys.Count)"
        Write-Host "Key count in b.k2: $($b.k2.Keys.Count)"


    .EXAMPLE
        PS C:\> $clonedObject = Copy-Object -DeepCopyObject $someObject

    .INPUTS
        It takes any kind of object as input which will be serialized and deserialized to create a copy.
        [object]

    .OUTPUTS
        [Deserialized.<object>]

    #>


    param (
        [Parameter(Mandatory = $true)]
        [object]
        $DeepCopyObject
    )

    $serialData = [System.Management.Automation.PSSerializer]::Serialize($DeepCopyObject)
    [System.Management.Automation.PSSerializer]::Deserialize($serialData)
}
#EndRegion './Private/Copy-Object.ps1' 61
#Region './Private/Expand-RsopHashtable.ps1' 0
function Expand-RsopHashtable
{
    param (
        [Parameter()]
        [object]
        $InputObject,

        [Parameter()]
        [switch]
        $IsArrayValue,

        [Parameter()]
        [int]
        $Depth,

        [Parameter()]
        [switch]
        $AddSourceInformation
    )

    $Depth++

    if ($null -eq $InputObject)
    {
        return $null
    }

    if ($InputObject -is [System.Collections.IDictionary])
    {
        $newObject = @{}
        $keys = [string[]]$InputObject.Keys
        foreach ($key in $keys)
        {
            $newObject.$key = Expand-RsopHashtable -InputObject $InputObject[$key] -Depth $Depth -AddSourceInformation:$AddSourceInformation
        }

        [ordered]@{} + $newObject
    }
    elseif ($InputObject -is [System.Collections.IList])
    {
        $doesUseYamlArraySyntax = [bool]($InputObject.Count - 1)
        if (-not $doesUseYamlArraySyntax)
        {
            $depth--
        }
        $items = foreach ($item in $InputObject)
        {
            Expand-RsopHashtable -InputObject $item -IsArrayValue:$doesUseYamlArraySyntax -Depth $Depth -AddSourceInformation:$AddSourceInformation
        }
        $items
    }
    elseif ($InputObject -is [pscredential])
    {
        $cred = $InputObject.GetNetworkCredential()
        $cred = "$($cred.UserName)@$($cred.Domain)$(if($cred.Domain){':'})$('*' * $cred.Password.Length)" | Add-Member -Name __File -MemberType NoteProperty -Value $InputObject.__File -PassThru

        Get-RsopValueString -InputString $cred -Key $key -Depth $depth -IsArrayValue:$IsArrayValue -AddSourceInformation:$AddSourceInformation
    }
    else
    {
        Get-RsopValueString -InputString $InputObject -Key $key -Depth $depth -IsArrayValue:$IsArrayValue -AddSourceInformation:$AddSourceInformation
    }
}
#EndRegion './Private/Expand-RsopHashtable.ps1' 64
#Region './Private/Get-DatumType.ps1' 0
function Get-DatumType
{
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [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' 32
#Region './Private/Get-MergeStrategyFromString.ps1' 0
function Get-MergeStrategyFromString
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter()]
        [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/Get-RsopValueString.ps1' 0
function Get-RsopValueString
{
    param (
        [Parameter(Mandatory = $true)]
        [object]
        $InputString,

        [Parameter(Mandatory = $true)]
        [string]
        $Key,

        [Parameter()]
        [int]$Depth,

        [Parameter()]
        [switch]$IsArrayValue,

        [Parameter()]
        [switch]
        $AddSourceInformation
    )

    if (-not $AddSourceInformation)
    {
        $InputString.psobject.BaseObject
    }
    else
    {
        $fileInfo = (Get-DatumSourceFile -Path $InputString.__File)

        $i = if ($env:DatumRsopIndentation)
        {
            $env:DatumRsopIndentation
        }
        else
        {
            120
        }

        $i = if ($IsArrayValue)
        {
            $Depth--
            $i - ("$InputString".Length)
        }
        else
        {
            $i - ($Key.Length + "$InputString".Length)
        }

        $i -= [System.Math]::Max(0, ($depth) * 2)
        "{0}$(if ($fileInfo) { ""{1, $i}"" })" -f $InputString, $fileInfo
    }
}
#EndRegion './Private/Get-RsopValueString.ps1' 54
#Region './Private/Invoke-DatumHandler.ps1' 0
function Invoke-DatumHandler
{
    <#
    .SYNOPSIS
        Invokes the configured datum handlers.

    .DESCRIPTION
        This function goes through all datum handlers configured in the 'datum.yml'. For all handlers, it calls the test function
        first that identifies if the particular handler should be invoked at all for the given InputString. The test function
        look for a prefix and suffix in orer to know if a handler should be called. For the handler 'Datum.InvokeCommand' the
        prefix is '[x=' and the siffix '=]'.

        Let's assume the handler is defined in a module named 'Datum.InvokeCommand'. The handler is introduced in the 'datum.yml'
        like this:

        DatumHandlers:
            Datum.InvokeCommand::InvokeCommand:
                SkipDuringLoad: true

        The name of the function that checks if the handler should be called is constructed like this:

            <FilterModuleName>\Test-<FilterName>Filter

        Considering the definition in the 'datum.yml', the actual function name will be:

            Datum.InvokeCommand\Test-InvokeCommandFilter

        Same rule applies for the action function that is actually the handler. Datum searches a function with the name

            <FilterModuleName>\Invoke-<FilterName>Action

        which will be in case of the filter module named 'Datum.InvokeCommand' and the filter name 'InvokeCommand':

            Datum.InvokeCommand\Invoke-InvokeCommandAction

    .EXAMPLE
        This sample calls the handlers defined in the 'Datum.yml' on the value '[x= { Get-Date } =]'. Only a handler will
        be invoked that has the prefix '[x=' and the siffix '=]'.

        PS C:\> $d = New-DatumStructure -DefinitionFile .\tests\Integration\assets\DscWorkshopConfigData\Datum.yml
        PS C:\> $result = $nul
        PS C:\> Invoke-DatumHandler -InputObject '[x= { Get-Date } =]' -DatumHandlers $d.__Definition.DatumHandlers -Result ([ref]$result)
        PS C:\> $result #-> Thursday, March 24, 2022 1:54:51 AM

    .INPUTS
        [object]

    .OUTPUTS
        Whatever the datum handler returns.

    .NOTES

    #>


    param (
        [Parameter(Mandatory = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [AllowNull()]
        [hashtable]
        $DatumHandlers,

        [Parameter()]
        [ref]$Result
    )

    $return = $false

    foreach ($handler in $DatumHandlers.Keys)
    {
        if ($DatumHandlers.$handler.SkipDuringLoad -and (Get-PSCallStack).Command -contains 'Get-FileProviderData')
        {
            continue
        }

        $filterModule, $filterName = $handler -split '::'
        if (-not (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 -Name ('{0}\Invoke-{1}Action' -f $filterModule, $filterName) -ErrorAction SilentlyContinue)
                {
                    $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."$paramName" = $var[0].Value
                        }
                    }
                    $internalResult = (&$actionCommand @actionParams)
                    if ($null -eq $internalResult)
                    {
                        $Result.Value = [string]::Empty
                    }

                    $Result.Value = $internalResult
                    return $true
                }
            }
            catch
            {
                #If true, datum handlers throwing errors will stop the whole compilation process. This is usually wanted to make sure
                #that the datum handlers are working as expected and your data / RSOP does not contain invalid or incomplete data.
                $throwOnError = [bool]$datum.__Definition.DatumHandlersThrowOnError

                if ($throwOnError)
                {
                    Write-Error -ErrorRecord $_ -ErrorAction Stop
                }
                else
                {
                    Write-Warning "Error using Datum Handler '$Handler', the error was: '$($_.Exception.Message)'. Returning InputObject ($InputObject)."
                    $Result = $InputObject
                    return $false
                }
            }
        }
    }

    return $return
}
#EndRegion './Private/Invoke-DatumHandler.ps1' 139
#Region './Private/Merge-DatumArray.ps1' 0
function Merge-DatumArray
{
    [OutputType([System.Collections.ArrayList])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object]
        $ReferenceArray,

        [Parameter(Mandatory = $true)]
        [object]
        $DifferenceArray,

        [Parameter()]
        [hashtable]
        $Strategy = @{},

        [Parameter()]
        [hashtable]
        $ChildStrategies = @{
            '^.*' = $Strategy
        },

        [Parameter(Mandatory = $true)]
        [string]
        $StartingPath
    )

    Write-Debug -Message "`tMerge-DatumArray -StartingPath <$StartingPath>"
    $knockout_prefix = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^')
    $hashArrayStrategy = $Strategy.merge_hash_array
    Write-Debug -Message "`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 -Message "`t`tMERGING Array of Hashtables"
        if (-not $hashArrayStrategy -or $hashArrayStrategy -match 'MostSpecific')
        {
            Write-Debug -Message "`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 -Message "`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 -Message "`t`t`t .. Working on Merged Element $($mergedArray.Count)`r`n"
                    # if no PropertyNames defined, use all Properties of $RefItem
                    if (-not $propertyNames)
                    {
                        Write-Debug -Message "`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
                        }
                        (-not (Compare-Hashtable @compareHashParams))
                    }
                    Write-Debug -Message "`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 (-not $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 -Message "`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 (-not $propertyNames)
                {
                    Write-Debug -Message "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys"
                    $propertyNames = $referenceItem.Keys
                }

                $mergedArray = [System.Collections.ArrayList]::new()
                $ReferenceArray | ForEach-Object {
                    $currentRefItem = $_
                    if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentRefItem -DifferenceHashtable $_ ) }))
                    {
                        $null = $mergedArray.Add(([ordered]@{} + $_))
                    }
                }

                $DifferenceArray | ForEach-Object {
                    $currentDiffItem = $_
                    if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentDiffItem -DifferenceHashtable $_ ) }))
                    {
                        $null = $mergedArray.Add(([ordered]@{} + $_))
                    }
                }
            }
        }
    }

    $mergedArray
}
#EndRegion './Private/Merge-DatumArray.ps1' 172
#Region './Private/Merge-Hashtable.ps1' 0
function Merge-Hashtable
{
    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        # [hashtable] These should stay ordered
        [Parameter(Mandatory = $true)]
        [object]
        $ReferenceHashtable,

        # [hashtable] These should stay ordered
        [Parameter(Mandatory = $true)]
        [object]
        $DifferenceHashtable,

        [Parameter()]
        $Strategy = @{
            merge_hash           = 'hash'
            merge_baseType_array = 'MostSpecific'
            merge_hash_array     = 'MostSpecific'
            merge_options        = @{
                knockout_prefix = '--'
            }
        },

        [Parameter()]
        [hashtable]
        $ChildStrategies = @{},

        [Parameter()]
        [string]
        $ParentPath
    )

    Write-Debug -Message "`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 -Message "`t Knockout Prefix Matcher: $knockoutPrefixMatcher"

    $knockedOutKeys = $ReferenceHashtable.Keys.Where{ $_ -match $knockoutPrefixMatcher }.ForEach{ $_ -replace $knockoutPrefixMatcher }
    Write-Debug -Message "`t Knockedout Keys: [$($knockedOutKeys -join ', ')] from reference Hashtable Keys [$($ReferenceHashtable.keys -join ', ')]"

    foreach ($currentKey in $DifferenceHashtable.keys)
    {
        Write-Debug -Message "`t CurrentKey: $currentKey"
        if ($currentKey -in $knockedOutKeys)
        {
            Write-Debug -Message "`t`tThe Key $currentkey is knocked out from the reference Hashtable."
        }
        elseif ($currentKey -match $knockoutPrefixMatcher -and -not $ReferenceHashtable.Contains(($currentKey -replace $knockoutPrefixMatcher)))
        {
            # it's a knockout coming from a lower level key, it should only apply down from here
            Write-Debug -Message "`t`tKnockout prefix found for $currentKey in Difference hashtable, and key not set in Reference hashtable"
            if (-not $ReferenceHashtable.Contains($currentKey))
            {
                Write-Debug -Message "`t`t..adding knockout prefixed key for $curretKey to block further merges"
                $clonedReference.Add($currentKey, $null)
            }
        }
        elseif (-not $ReferenceHashtable.Contains($currentKey) )
        {
            #if the key does not exist in reference ht, create it using the DiffHt's value
            Write-Debug -Message "`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 -DatumObject $ReferenceHashtable[$currentKey]
            $diffHashItemValueType = Get-DatumType -DatumObject $DifferenceHashtable[$currentKey]
            Write-Debug -Message "for Key $currentKey REF:[$refHashItemValueType] | DIFF:[$diffHashItemValueType]"
            if ($ParentPath)
            {
                $childPath = Join-Path -Path $ParentPath -ChildPath $currentKey
            }
            else
            {
                $childPath = $currentKey
            }

            switch ($refHashItemValueType)
            {
                'hashtable'
                {
                    if ($Strategy.merge_hash -eq 'deep')
                    {
                        Write-Debug -Message "`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 -Message "`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 -Message "`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 -Message "`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 -Message "`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 -Message "`t .. Datum Merged for path $childPath"
                }
            }
        }
    }

    return $clonedReference
}
#EndRegion './Private/Merge-Hashtable.ps1' 166
#Region './Public/Clear-DatumRsopCache.ps1' 0
function Clear-DatumRsopCache
{
    [CmdletBinding()]

    param ()

    if ($script:rsopCache.Count)
    {
        $script:rsopCache.Clear()
        Write-Verbose -Message 'Datum RSOP Cache cleared'
    }
}
#EndRegion './Public/Clear-DatumRsopCache.ps1' 13
#Region './Public/ConvertTo-Datum.ps1' 0
function ConvertTo-Datum
{
    param (
        [Parameter(ValueFromPipeline = $true)]
        [object]
        $InputObject,

        [Parameter()]
        [AllowNull()]
        [hashtable]
        $DatumHandlers = @{}
    )

    process
    {
        $result = $null

        if ($null -eq $InputObject)
        {
            return $null
        }

        if ($InputObject -is [System.Collections.IDictionary])
        {
            if (-not $file -and $InputObject.__File)
            {
                $file = $InputObject.__File
            }

            $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) | Add-Member -Name __File -MemberType NoteProperty -Value "$file" -PassThru -Force
        }
        elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
        {
            $collection = @(
                foreach ($object in $InputObject)
                {
                    if (-not $file -and $object.__File)
                    {
                        $file = $object.__File
                    }
                    ConvertTo-Datum -InputObject $object -DatumHandlers $DatumHandlers
                }
            )

            , $collection
        }
        elseif (($InputObject -is [DatumProvider]) -and $InputObject -isnot [pscredential])
        {
            if (-not $file -and $InputObject.__File)
            {
                $file = $InputObject.__File
            }

            $hash = [ordered]@{}

            foreach ($property in $InputObject.PSObject.Properties)
            {
                $hash[$property.Name] = ConvertTo-Datum -InputObject $property.Value -DatumHandlers $DatumHandlers | Add-Member -Name __File -MemberType NoteProperty -Value $File.FullName -PassThru -Force
            }

            $hash
        }
        # if there's a matching filter, process associated command and return result
        elseif ($DatumHandlers.Count -and (Invoke-DatumHandler -InputObject $InputObject -DatumHandlers $DatumHandlers -Result ([ref]$result)))
        {
            if (-not $file -and $InputObject.__File)
            {
                $file = $InputObject.__File
            }

            if ($result)
            {
                if (-not $result.__File -and $InputObject.__File)
                {
                    $result | Add-Member -Name __File -Value "$($InputObject.__File)" -MemberType NoteProperty -PassThru -Force
                }
                elseif (-not $result.__File -and $file)
                {
                    $result | Add-Member -Name __File -Value "$($file)" -MemberType NoteProperty -PassThru -Force
                }
                else
                {
                    $result
                }
            }
            else
            {
                Write-Verbose "Datum handlers for '$InputObject' returned '$null'"
                $null
            }
        }
        else
        {
            if (-not $file -and $InputObject.__File)
            {
                $file = $InputObject.__File
            }

            if ($file -and -not $InputObject.__File)
            {
                $InputObject | Add-Member -Name __File -Value "$file" -MemberType NoteProperty -PassThru -Force
            }
            else
            {
                $InputObject
            }
        }
    }
}
#EndRegion './Public/ConvertTo-Datum.ps1' 116
#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 = {},

        [Parameter()]
        [switch]
        $IgnoreCache,

        [Parameter()]
        [switch]
        $IncludeSource,

        [Parameter()]
        [switch]
        $RemoveSource
    )

    if (-not $script:rsopCache)
    {
        $script:rsopCache = @{}
    }

    if ($Filter.ToString() -ne ([System.Management.Automation.ScriptBlock]::Create( {})).ToString())
    {
        Write-Verbose "Filter: $($Filter.ToString())"
        $AllNodes = [System.Collections.Hashtable[]]$AllNodes.Where($Filter)
        Write-Verbose "Node count after applying filter: $($AllNodes.Count)"
    }

    foreach ($node in $AllNodes)
    {
        if (-not $node.Name)
        {
            $node.Name = $node.NodeName
        }

        $null = $node | ConvertTo-Datum -DatumHandlers $Datum.__Definition.DatumHandlers

        if (-not $script:rsopCache.ContainsKey($node.Name) -or $IgnoreCache)
        {
            Write-Verbose "Key not found in the cache: '$($node.Name)'. Creating RSOP..."
            $rsopNode = $node.Clone()

            $configurations = Resolve-NodeProperty -PropertyPath $CompositionKey -Node $node -DatumTree $Datum -DefaultValue @()
            $rsopNode."$CompositionKey" = $configurations

            $configurations.ForEach{
                $value = Resolve-NodeProperty -PropertyPath $_ -DefaultValue @{} -Node $node -DatumTree $Datum
                $rsopNode."$_" = $value
            }

            $lcmConfigKeyName = $datum.__Definition.DscLocalConfigurationManagerKeyName
            if ($lcmConfigKeyName)
            {
                $lcmConfig = Resolve-NodeProperty -PropertyPath $lcmConfigKeyName -DefaultValue $null
                if ($lcmConfig)
                {
                    $rsopNode.LcmConfig = $lcmConfig
                }
                else
                {
                    Write-Host -Object "`tWARNING: 'DscLocalConfigurationManagerKeyName' is defined in the 'datum.yml' but did not return a result for node '$($node.Name)'" -ForegroundColor Yellow
                }
            }

            $clonedRsopNode = Copy-Object -DeepCopyObject $rsopNode
            $clonedRsopNode = ConvertTo-Datum -InputObject $clonedRsopNode -DatumHandlers $Datum.__Definition.DatumHandlers
            $script:rsopCache."$($node.Name)" = $clonedRsopNode
        }
        else
        {
            Write-Verbose "Key found in the cache: '$($node.Name)'. Retrieving RSOP from cache."
        }

        if ($IncludeSource)
        {
            Expand-RsopHashtable -InputObject $script:rsopCache."$($node.Name)" -Depth 0 -AddSourceInformation
        }
        elseif ($RemoveSource)
        {
            Expand-RsopHashtable -InputObject $script:rsopCache."$($node.Name)" -Depth 0
        }
        else
        {
            $script:rsopCache."$($node.Name)"
        }
    }
}
#EndRegion './Public/Get-DatumRsop.ps1' 105
#Region './Public/Get-DatumRsopCache.ps1' 0
function Get-DatumRsopCache
{
    [CmdletBinding()]

    param ()

    if ($script:rsopCache.Count)
    {
        $script:rsopCache
    }
    else
    {
        $script:rsopCache = @{}
        Write-Verbose 'The Datum RSOP Cache is empty.'
    }
}
#EndRegion './Public/Get-DatumRsopCache.ps1' 17
#Region './Public/Get-DatumSourceFile.ps1' 0
function Get-DatumSourceFile
{
    <#
    .SYNOPSIS
        Gets the source file for the given datum.
    .DESCRIPTION

        This command gets the relative source file for the given datum. The source file path
        is relative to the current directory and skips the first directory in the path.

    .EXAMPLE
        PS C:\> Get-DatumSourceFile -Path D:\git\datum\tests\Integration\assets\DscWorkshopConfigData\Roles\DomainController.yml

        This command returns the source file path like this:
            assets\DscWorkshopConfigData\Roles\DomainController

    .INPUTS
        string

    .OUTPUTS
        string
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Path
    )

    if (-not $Path)
    {
        return [string]::Empty
    }

    try
    {
        $p = Resolve-Path -Path $Path -Relative -ErrorAction Stop
        $p = $p -split '\\'
        $p[-1] = [System.IO.Path]::GetFileNameWithoutExtension($p[-1])
        $p[2..($p.Length - 1)] -join '\'
    }
    catch
    {
        Write-Verbose 'Get-DatumSourceFile: nothing to catch here'
    }
}
#EndRegion './Public/Get-DatumSourceFile.ps1' 48
#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
{
    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Strategies,

        [Parameter(Mandatory = $true)]
        [string]
        $PropertyPath
    )

    Write-Debug -Message "`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 -Message "`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 -Message "`t Strategy matching regex $strategyKey"
    }
    else
    {
        Write-Debug -Message "`t No Strategy found"
        return
    }

    Write-Debug -Message "`t StrategyKey: $strategyKey"
    if ($Strategies[$strategyKey] -is [string])
    {
        Write-Debug -Message "`t Returning Strategy $strategyKey from String '$($Strategies[$strategyKey])'"
        Get-MergeStrategyFromString -MergeStrategy $Strategies[$strategyKey]
    }
    else
    {
        Write-Debug -Message "`t Returning Strategy $strategyKey of type '$($Strategies[$strategyKey].Strategy)'"
        $Strategies[$strategyKey]
    }
}
#EndRegion './Public/Get-MergeStrategyFromPath.ps1' 48
#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)"

    $result = $null
    if ($ReferenceDatum -is [array])
    {
        $datumItems = @()
        foreach ($item in $ReferenceDatum)
        {
            if (Invoke-DatumHandler -InputObject $item -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result))
            {
                $datumItems += ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers
            }
            else
            {
                $datumItems += $item
            }
        }
        $ReferenceDatum = $datumItems
    }
    else
    {
        if (Invoke-DatumHandler -InputObject $ReferenceDatum -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result))
        {
            $ReferenceDatum = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers
        }
    }

    if ($DifferenceDatum -is [array])
    {
        $datumItems = @()
        foreach ($item in $DifferenceDatum)
        {
            if (Invoke-DatumHandler -InputObject $item -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result))
            {
                $datumItems += ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers
            }
            else
            {
                $datumItems += $item
            }
        }
        $DifferenceDatum = $datumItems
    }
    else
    {
        if (Invoke-DatumHandler -InputObject $DifferenceDatum -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result))
        {
            $DifferenceDatum = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers
        }
    }

    $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' 213
#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
                $DatumHierarchyDefinition.DatumDefinitionFile = $DefinitionFile
                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 | Where-Object { $_.ExportedCommands.Keys -match 'New-Datum(\w+)Provider' }).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' 160
#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

    #Invoke datum handlers
    $PathPrefixes = $PathPrefixes | ConvertTo-Datum -DatumHandlers $datum.__Definition.DatumHandlers

    # 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 (
                    [Parameter()]
                    $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' 242
#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