Datum.psm1

Class FileProvider {
    hidden $Path
    hidden [hashtable] $Store
    hidden [hashtable] $DatumHierarchyDefinition
    hidden [hashtable] $StoreOptions
    hidden [hashtable] $DatumHandlers

    FileProvider ($Path,$Store,$DatumHierarchyDefinition)
    {
        $this.Store = $Store
        $this.DatumHierarchyDefinition = $DatumHierarchyDefinition
        $this.StoreOptions = $Store.StoreOptions
        $this.Path = Get-Item $Path -ErrorAction SilentlyContinue
        $this.DatumHandlers = $DatumHierarchyDefinition.DatumHandlers

        $Result = Get-ChildItem $path | ForEach-Object {
            if($_.PSisContainer) {
                $val = [scriptblock]::Create("New-DatumFileProvider -Path `"$($_.FullName)`" -StoreOptions `$this.DataOptions -DatumHierarchyDefinition `$this.DatumHierarchyDefinition")
                $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val
            }
            else {
                $val = [scriptblock]::Create("Get-FileProviderData -Path `"$($_.FullName)`" -DatumHandlers `$this.DatumHandlers")
                $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val
            }
        }
    }
}

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"
    }
}

Class SecureDatum {
    [hashtable] hidden  $UnprotectParams
    SecureDatum($Object,[hashtable]$UnprotectParams) {
        $this.UnprotectParams = $UnprotectParams
        if($Object -is [hashtable]) {
            $Object = [PSCustomObject]$Object
        }

        if ($Object -is [PSCustomObject]) {
            foreach ($Property in $Object.PSObject.Properties.name) {
                $MemberTypeParams = @{
                    MemberType = 'NoteProperty'
                    Name = $Property
                    Value = ([SecureDatum]::GetObject($Object.$Property,$UnprotectParams))
                }
                if ($MemberTypeParams.Value -is [scriptblock]) {
                    $MemberTypeParams.MemberType = 'ScriptProperty'
                }
                $This | Add-Member @MemberTypeParams
            }
        }
    }
    [string] ToString()
    {
        return "{$($this.PSObject.Properties.Name -join ', ')}"
    }
    static [object] GetObject($object,$UnprotectParams)
    {
        if($null -eq $object) {
            return $null
        }
        elseif($object -is [PSCustomObject] -or
            $object -is [hashtable]) {
            return ([SecureDatum]::new($object,$UnprotectParams))
        }
        elseif ($object -is [System.Collections.IEnumerable] -and $object -isnot [string]) {
            $collection = @()
            $collection = foreach ($item in $object) {
                [SecureDatum]::GetObject($item,$UnprotectParams)
            }
            return $collection
        }
        elseif($object -is [string] -and $object -match "^\[ENC=[\w\W]*\]$") {
            $UnprotectScriptBlock = "
                `$Base64Data = `"$object`"
                `[SecureDatum]::Unprotect(`$Base64Data.Trim(),`$this.UnprotectParams)
                "

            return ([scriptblock]::Create($UnprotectScriptBlock))
        }
        else {
            return $object
        }
    }
    static [object] Unprotect($object,$UnprotectParams)
    {
        return (Unprotect-Datum -Base64Data $object @UnprotectParams)
    }
}

function ConvertTo-Datum
{
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject,

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

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

        # if There's a matching filter, process associated command and return result
        if($HandlerNames = [string[]]$DatumHandlers.Keys) {
            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
                            foreach( $ParamName in $ActionCommand.Parameters.keys ) {
                                if( $ParamName -in $CommandOptions ) {
                                    $ActionParams.add($ParamName,$Datumhandlers.$handler.CommandOptions[$ParamName])
                                }
                                elseif($ValueInScope = Get-Variable -name $ParamName -ErrorAction SilentlyContinue -ValueOnly ){
                                    $ActionParams.add($ParamName,$ValueInScope)
                                }
                            }
                            return (&$ActionCommand @ActionParams)
                        }
                    }
                    catch {
                        Write-Warning "Error using Datum Handler $Handler, returning Input Object"
                        $InputObject
                    }
                }
            }
        }

        if ($InputObject -is [System.Collections.Hashtable] -or ($InputObject -is [System.Collections.Specialized.OrderedDictionary])) {
            $hashKeys = [string[]]$InputObject.Keys
            foreach ($Key in $hashKeys) {
                $InputObject[$Key] = ConvertTo-Datum -InputObject $InputObject[$Key] -DatumHandlers $DatumHandlers
            }
            $InputObject
        }
        elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
        {
            $collection = @(
                foreach ($object in $InputObject) { ConvertTo-Datum -InputObject $object -DatumHandlers $DatumHandlers }
            )

            Write-Output -NoEnumerate $collection
        }
        elseif ($InputObject -is [psobject])
        {
            $hash = [ordered]@{}

            foreach ($property in $InputObject.PSObject.Properties)
            {
                $hash[$property.Name] = ConvertTo-Datum -InputObject $property.Value -DatumHandlers $DatumHandlers
            }

            $hash
        }
        else
        {
            $InputObject
        }
    }
}

function ConvertTo-ProtectedDatum
{###########ConvertTo-DatumSecureObjectReader
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject,

        $UnprotectOptions
    )

    process
    {
        if ($UnprotectOptions.contains('ClearTextPassword')) {
            $UnprotectOptions['password'] = $UnprotectOptions.ClearTextPassword |
                ConvertTo-SecureString -AsPlainText -force
            $null = $UnprotectOptions.remove('ClearTextPassword')
        }
        elseif ($UnprotectOptions.contains('SecureStringPassword')) {
            $UnprotectOptions['password'] = $UnprotectOptions.SecureStringPassword |
                ConvertTo-SecureString
            $null = $UnprotectOptions.remove('SecureStringPassword')
        }
       [SecureDatum]::GetObject($InputObject,$UnprotectOptions)
    }
}

function Get-MergeStrategyFromString {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [String]
        $MergeStrategy
    )

    switch -regex ($MergeStrategy) {
        '^First$|^MostSpecific$' {
            @{
                strategy = 'MostSpecific'
            }
        }

        '^Unique$|^ArrayUniques$' {
            @{
                strategy = 'Unique'
            }
        }

        '^hash$|^MergeTopKeys$' {
            @{
                strategy = 'hash'
                options = @{
                    knockout_prefix    = '--'
                    sort_merged_arrays = $false
                    merge_hash_arrays  = $false
                }
            }
        }

        '^deep$|^MergeRecursively$' {
            @{
                strategy = 'deep'
                options = @{
                    knockout_prefix    = '--'
                    sort_merged_arrays = $false
                    merge_hash_arrays  = $false
                }
            }
        }
        default {
            Write-Verbose "Couldn't Match the strategy $MergeStrategy"
            @{
                strategy = 'MostSpecific'
            }
        }
    }

}

function Merge-Hashtable {
    [outputType([hashtable])]
    [cmdletBinding()]
    Param(
        [hashtable]
        $ReferenceHashtable,

        [hashtable]
        $DifferenceHashtable,

        [validateScript(
            {   $_ -as [hashtable] -and $_.strategy -in @('hash','deep') -or
                $_ -in @('hash','deep')
            }
        )]
        $Strategy = @{
                Strategy = 'deep'
                options = @{
                    knockoutprefix = '--'
                }
            },

        $ChildStrategies = @{},

        [string]
        $ParentPath
    )


    $clonedReference = $ReferenceHashtable.Clone()
    if ($Strategy.options.knockout_prefix) {
        $KnockoutPrefix = $Strategy.options.knockout_prefix
        $KnockoutPrefixMatcher = [regex]::escape($KnockoutPrefix).insert(0,'^')
        Write-Debug "Knockout Prefix Matcher: $knockoutPrefixMatcher"
    }

    if($strategy -eq 'deep' -or $strategy.Strategy -eq 'deep') {
        $deepmerge = $true
    }
    else {
        $deepmerge = $false
    }

    $knockedOutKeys = $ReferenceHashtable.keys.where{$_ -match $KnockoutPrefixMatcher}.foreach{$_ -replace $KnockoutPrefixMatcher}
    Write-Debug "Knockout Keys: $($knockedOutKeys -join ', '); Ref Hashtable Keys $($ReferenceHashtable.keys -join ', ')"

    foreach ($currentKey in $DifferenceHashtable.keys) {
        Write-Debug "CurrentKey: $currentKey"
        if($currentKey -in $knockedOutKeys) {
            Write-Debug "`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 "`tKnockout prefix found for $currentKey in Difference hashtable, and key not set in Reference hashtable"
            if(!$ReferenceHashtable.contains($currentKey)) {
                Write-Debug "`t..adding knockout prefixed key for $curretKey to block further merges"
                $clonedReference.add($currentKey,$null)
            }
            #Write-Warning "Removed key $($currentKey -replace $KnockoutPrefixMatcher) as we found the knockout prefix $KnockoutPrefix in the difference object"
            #$clonedReference.remove(($currentKey -replace $KnockoutPrefixMatcher))
        }
        elseif (!$ReferenceHashtable.contains($currentKey) )  {
            #if the key does not exist in reference ht, create it using the DiffHt's value
            Write-Debug "`tAdded Key $currentKey using the DifferenceHashtable value: $($DifferenceHashtable[$currentKey]| Format-List * | out-String)"
            $clonedReference.add($currentKey,$DifferenceHashtable[$currentKey])
        }
        else { #the key exists, and it's not a knockout entry
            if ($deepmerge -and ($ReferenceHashtable[$currentKey] -as [hashtable] -or ($ReferenceHashtable[$currentKey] -is [System.Collections.IEnumerable] -and $ReferenceHashtable[$currentKey] -isnot [string]))) {
                # both are hashtables and we're in Deepmerge mode
                Write-Debug "`t .. Merging Datums at current path $ParentPath\$CurrentKey"
                $subMerge = Merge-Datum -StartingPath (Join-Path  $ParentPath $currentKey) -ReferenceDatum $ReferenceHashtable[$currentKey] -DifferenceDatum $DifferenceHashtable[$currentKey] -Strategies $ChildStrategies
                Write-Debug "# Submerge $($submerge|ConvertTo-Json)."
                $clonedReference[$currentKey]  = $subMerge
            }  ####################### ---> add array merge and hashtable[] merge here (hashtable[] merge based on defined subkey)
            else {
                #one is not an hashtable or we're not in deepmerge mode, leave the ClonedReference as-is
                Write-verbose "`tDeepmerge: $deepmerge; Ref[$currentkey] type $($ReferenceHashtable[$currentKey].GetType()); Diff[$currentkey] type $($DifferenceHashtable[$currentKey].GetType())"
            }
        }
    }

    return $clonedReference

}

<#
$a = @{
    keya = 1
    keyb = 2
    keyc = 3
    '--keye' = $null
}
 
$b = @{
    '--keya' = $null # removing keya
    keyb = 22 # won't override keyb
    keyd = 33 # will add keyd with value
    keye = 44 # keye should never be added, as it's removed from the ref ht
}
 
# simple merge: create keys from $b that do not exist in $a, remove --keys
 
$d = [ordered]@{
    a = [ordered]@{
        x = 111
        y = 222
        z = 333
    }
    b = 2
    c = 3
    d = 4
    e = [ordered]@{
        x = 111
        '--y' = $null
    }
}
 
$c = @{
    b = 0 #already defined, should ignore
    '--c' = $null #doesn't remove the key c from $c as it would violate the hierarchy
    #d missing intentionally, already defined
    e = @{
        # key x omitted, already present
        y = 222 # this key 'y' should be added to $c.e
        z = 333 # this key 'z' should be added to $c.e
    }
}
 
$e = [ordered]@{
    RootKey1 = [ordered]@{
        subkey11 = [ordered]@{
            subkey111 = 111
            #'--Subkey112' = $null
            Subkey113 = 113
        }
        subkey12 = [ordered]@{
            subkey123 = 123
            subkey124 = 124
        }
    }
    RootKey2 = [ordered]@{
        Subkey21 = [ordered]@{
            Subkey211 = 211
            Subkey212 = 212
            Subkey213 = 213
        }
        Subkey22 = @(
            222
            223
            224
        )
        SubKey23 = @(
            [ordered]@{Name = 1; val1 = 1}
            [ordered]@{Name = 2; val1 = 2}
            [ordered]@{Name = 3; val1 = 3}
        )
    }
}
 
$f = [ordered]@{
    RootKey1 = [ordered]@{
        subkey11 = [ordered]@{
            subkey111 = 111
            Subkey112 = 112
            Subkey113 = 113
        }
        subkey12 = [ordered]@{
            subkey123 = 123
            subkey124 = 124
        }
    }
    RootKey2 = [ordered]@{
        Subkey21 = [ordered]@{
            Subkey211 = 2110
            Subkey212 = 2120
            Subkey213 = 2130
        }
        Subkey22 = @(
            221
        )
        SubKey23 = @(
            [ordered]@{Name = 1; val1 = 1}
            [ordered]@{Name = 2; val1 = 3}
            [ordered]@{Name = 3}
        )
    }
}
 
$MergeParams = @{
    StartingPath = 'root'
 
    ReferenceDatum = $e
 
    DifferenceDatum = $f
 
    Strategies = @{
        'root' = 'deep'
        'root\rootkey2\Subkey22' = 'Unique'
        'root\rootkey2\Subkey23' = 'Unique'
        '^.*' = 'deep'
    }
}
Merge-Datum @MergeParams
#>


function Get-FileProviderData {
    [CmdletBinding()]
    Param(
        $Path,

        [AllowNull()]
        $DatumHandlers = @{}
    )
    Write-Verbose "Getting File Provider Data for Path: $Path"
    $File = Get-Item -Path $Path
    switch ($File.Extension) {
        '.psd1' { Import-PowerShellDataFile $File           | ConvertTo-Datum -DatumHandlers $DatumHandlers }
        '.json' { ConvertFrom-Json (Get-Content -Raw $Path) | ConvertTo-Datum -DatumHandlers $DatumHandlers }
        '.yml'  { ConvertFrom-Yaml (Get-Content -raw $Path) -ordered | ConvertTo-Datum -DatumHandlers $DatumHandlers }

        Default { Get-Content -Raw $Path }
    }
}

function Get-MergeStrategyFromPath {
    [CmdletBinding()]
    Param(
        $Strategies,

        $PropertyPath
    )
    Write-debug ">>> MergeStrategyFromPath $PropertyPath"
    # Select Relevant strategy
    # Use exact path match first
    # or try Regex in order
    if ($Strategies[$PropertyPath]) {
        $StrategyKey = $PropertyPath
        Write-debug "`tStrategy found for exact key $StrategyKey"
    }
    elseif($StrategyKey = [string]($Strategies.keys.where{$_.StartsWith('^') -and $_ -as [regex] -and $PropertyPath -match $_} | Select-Object -First 1)) {
        Write-debug "`tStrategy matching regex $StrategyKey"
    }
    else {
        Write-debug "`tNo Strategy found"
        return
    }
    Write-Debug "`tStrategyKey: $StrategyKey. $($Strategies[$StrategyKey].getType())"
    if( $Strategies[$StrategyKey] -is [string]) {
        Write-debug "`tReturning from String $($Strategies[$StrategyKey])"
        Get-MergeStrategyFromString $Strategies[$StrategyKey]
    }
    else {
        Write-Debug "`tReturning $($Strategies[$StrategyKey]|ConvertTo-Json)"
        $Strategies[$StrategyKey]
    }
}

function Invoke-ProtectedDatumAction {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')]
    Param (
        # Serialized Protected Data represented on Base64 encoding
        [Parameter(
             Mandatory
            ,Position=0
            ,ValueFromPipeline
            ,ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $InputObject,

        # By Password only for development / Test purposes
        [Parameter(
            ParameterSetName='ByPassword'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [String]
        $PlainTextPassword,

        # Specify the Certificate to be used by ProtectedData
        [Parameter(
            ParameterSetName='ByCertificate'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [String]
        $Certificate,

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Header = '[ENC=',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Footer = ']',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [Switch]
        $NoEncapsulation
    )
    Write-Debug "Decrypting Datum using ProtectedData"
    $params = @{}
    foreach($ParamKey in $PSBoundParameters.keys) {
        if($ParamKey -in @('InputObject','PlainTextPassword')) {
            switch ($ParamKey) {
                'PlainTextPassword' { $params.add('password',(ConvertTo-SecureString -AsPlainText -Force $PSBoundParameters[$ParamKey])) }
                'InputObject' { $params.add('Base64Data',$InputObject) }
            }
        }
        else {
            $params.add($ParamKey,$PSBoundParameters[$ParamKey])
        }
    }

    UnProtect-Datum @params

}

function Invoke-TestHandlerAction {
    Param(
        $Password,

        $test,

        $Datum
    )
@"
    Action: $handler
    Node: $($Node|FL *|Out-String)
    Params:
$($PSBoundParameters | Convertto-Json)
"@

}

function Merge-Datum {
    [CmdletBinding()]
    param (
        [string]
        $StartingPath,

        $ReferenceDatum,

        $DifferenceDatum,

        $Strategies = @{
            '^.*' = 'MostSpecific'
        }
    )

    Write-verbose "`r`n######## MERGE DATUM`r`nPATH: $StartingPath.`r`n`r`n"
    Write-Debug "`r`nStrategies : $($strategies|Convertto-Json)`r`n########"
    Write-Debug "REF $($ReferenceDatum|Convertto-JSon)"
    $Strategy = Get-MergeStrategyFromPath -Strategies $strategies -PropertyPath $startingPath -Verbose

    Write-Verbose "Strategy: $($Strategy | ConvertTo-Json)"
    # Merge with strategy
    $mergeParams = @{
        ReferenceHashtable  = $ReferenceDatum
        DifferenceHashtable = $DifferenceDatum
        Strategy = $Strategy
        ParentPath = $StartingPath
    }

    switch ($Strategy.Strategy) {
        'MostSpecific' { return $ReferenceDatum}
        'AllValues'    { return $DifferenceDatum }

        'Unique'       {
            if($ReferenceDatum -as [hashtable]) {
                $ReferenceDatum = @($ReferenceDatum)
            }

            if($DifferenceDatum -as [hashtable]) {
                $DifferenceDatum = @($DifferenceDatum)
            }

            if($ReferenceDatum -as [hashtable[]]) {
                # it's an array of Hashtable objects, merge it by uniqueness
                # compare those with same set of Keys, then compare values? or compare object for sum of keys
            }
            elseif ($ReferenceDatum -is [System.Collections.IEnumerable] -and $ReferenceDatum -isnot [string]) {
                # it's another type of collection
                # cast refdatum to object[], add $diffDatum values, select unique, return
                @($ReferenceDatum + $DifferenceDatum) | Select-Object -Unique
            }
        }

        'hash'         {
            # ignore non-hashtable elements (replace with empty hash)
            if(!($ReferenceDatum -as [hashtable])) {
                $mergeParams['ReferenceHashtable'] = @{}
            }

            if(!($DifferenceDatum -as [hashtable])) {
                $mergeParams['DifferenceHashtable'] = @{}
            }

            # merge top layer keys, ignore subkeys
            Merge-Hashtable @mergeParams
        }

        'deep' {
            if($ReferenceDatum -as [hashtable[]]) {
                Write-Debug "array of hashtable. Should merge by position, property or uniqueness"
                # it's an array of Hashtable, merge it by position, property, or uniqueness?
            }
            Write-Debug "adding Child Startegies: $($Strategies|ConvertTo-Json)"
            $mergeParams.Add('ChildStrategies',$Strategies)
            Merge-Hashtable @mergeParams
        }

    }

    # Strategy is MostSpecific --> No Merge
    # strategy is All Values --> No Merge, return all

    # Strategy is Unique --> cast to refdatum [object[]] + diffDatum | select Unique
    # Strategy is Hash --> Merge Keys.
    # Strategy is Deep
    # --> is Array or [object[]]Value
    # --> Merge Hash[]?
    # ---> No, only keep refDatum
    # ---> Uniques: cast to refdatum [object[]] + diffDatum | select Unique
    # ---> ByKey: Merge ArrayItem.Where{$_.key -match refArrayItem.Key}
    # --> SubMode? Deep or hash
    # ---> ByPosition: Merge Ref[itemIndex] with Diff[itemIndex]
    # --> SubMode? Deep or hash
    # --> is Hash/Ordered

}

function New-DatumFileProvider {
    Param(

        [alias('DataOptions')]
        [AllowNull()]
        $Store,

        [AllowNull()]
        $DatumHierarchyDefinition = @{},

        $Path = $Store.StoreOptions.Path
    )

    if (!$DatumHierarchyDefinition) {
        $DatumHierarchyDefinition = @{}
    }

    [FileProvider]::new($Path, $Store,$DatumHierarchyDefinition)
}

function New-DatumStructure {
    [CmdletBinding(
        DefaultParameterSetName = 'FromConfigFile'
    )]

    Param (
        [Parameter(
            Mandatory,
            ParameterSetName = 'DatumHierarchyDefinition'
        )]
        [Alias('Structure')]
        [hashtable]
        $DatumHierarchyDefinition,

        [Parameter(
            Mandatory,
            ParameterSetName = 'FromConfigFile'
        )]
        [io.fileInfo]
        $DefinitionFile
    )

    switch ($PSCmdlet.ParameterSetName) {
        'DatumHierarchyDefinition' {
            if ($DatumHierarchyDefinition.contains('DatumStructure')) {
                Write-debug "Loading Datum from Parameter"
            }
            elseif($DatumHierarchyDefinition.Path) {
                $DatumHierarchyFolder = $DatumHierarchyDefinition.Path
                Write-Debug "Loading default Datum from given path $DatumHierarchyFolder"
            }
            else {
                Write-Warning "Desperate attempt to load Datum from Invocation origin..."
                $CallStack = Get-PSCallstack
                $DatumHierarchyFolder = $CallStack[-1].psscritroot
                Write-Warning " ---> $DatumHierarchyFolder"
            }
        }

        'FromConfigFile' {
            if((Test-Path $DefinitionFile)) {
                $DefinitionFile = (Get-Item $DefinitionFile -ErrorAction Stop)
                Write-Debug "File $DefinitionFile found. Loading..."
                $DatumHierarchyDefinition = Get-FileProviderData $DefinitionFile.FullName
                if(!$DatumHierarchyDefinition.contains('ResolutionPrecedence')) {
                    Throw 'Invalid Datum Hierarchy Definition'
                }
                $DatumHierarchyFolder = $DefinitionFile.directory.FullName
                Write-Debug "Datum Hierachy Parent folder: $DatumHierarchyFolder"
            }
            else {
                Throw "Datum Hierarchy Configuration not found"
            }
        }
    }


    $root = @{}
    if($DatumHierarchyFolder -and !$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 !$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 $Store.clone())
            Path  = $store.StoreOptions.Path
        }

        # 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(!($Module = Get-Module $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
            ![io.path]::IsPathRooted($StoreParams.Path) -and
            $DatumHierarchyFolder
        ) {
            Write-Debug "Replacing Store Path with AbsolutePath"
            $StorePath = Join-Path $DatumHierarchyFolder $StoreParams.Path -Resolve -ErrorAction Stop
            $StoreParams['Path'] = $StorePath
        }

        if ($NewProvidercmd.Parameters.keys -contains 'DatumHierarchyDefinition') {
            Write-Debug "Adding DatumHierarchyDefinition to Store Params"
            $StoreParams.add('DatumHierarchyDefinition',$DatumHierarchyDefinition)
        }

        $storeObject = &$NewProvidercmd @StoreParams
        Write-Debug "Adding key $($store.storeName) to Datum root object"
        $root.Add($store.StoreName,$storeObject)
    }

    #return the Root Datum hashtable
    $root
}

#Requires -Modules ProtectedData

function Protect-Datum {
    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        # Serialized Protected Data represented on Base64 encoding
        [Parameter(
             Mandatory
            ,Position=0
            ,ValueFromPipeline
            ,ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [PSObject]
        $InputObject,

        # By Password only for development / Test purposes
        [Parameter(
            ParameterSetName='ByPassword'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [System.Security.SecureString]
        $Password,

        # Specify the Certificate to be used by ProtectedData
        [Parameter(
            ParameterSetName='ByCertificate'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [String]
        $Certificate,

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [Int]
        $MaxLineLength = 100,

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Header = '[ENC=',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Footer = ']',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [Switch]
        $NoEncapsulation

    )

    begin {
    }

    process {
        Write-Verbose "Deserializing the Object from Base64"

        $ProtectDataParams = @{
            InputObject = $InputObject
        }
        Write-verbose "Calling Protect-Data $($PSCmdlet.ParameterSetName)"
        Switch ($PSCmdlet.ParameterSetName) {
            'ByCertificae' { $ProtectDataParams.Add('Certificate',$Certificate)}
            'ByPassword'   { $ProtectDataParams.Add('Password',$Password)      }
        }

        $securedData =  Protect-Data @ProtectDataParams
        $xml = [System.Management.Automation.PSSerializer]::Serialize($securedData, 5)
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($xml)
        $Base64String = [System.Convert]::ToBase64String($bytes)

        if($MaxLineLength -gt 0) {
            $Base64DataBlock = [regex]::Replace($Base64String,"(.{$MaxLineLength})","`$1`r`n")
        }
        else {
            $Base64DataBlock = $Base64String
        }
        if(!$NoEncapsulation) {
            $Header,$Base64DataBlock,$Footer -join ''
        }
        else {
            $Base64DataBlock
        }
    }

}

Function Resolve-Datum {
    [cmdletBinding()]
    Param(
        [Parameter(
            Mandatory
        )]
        [string]
        $PropertyPath,

        [Parameter(
            Position = 1
        )]
        [Alias('Node')]
        $Variable = $ExecutionContext.InvokeCommand.InvokeScript('$Node'),

        [string]
        $VariableName = 'Node',

        [Alias('DatumStructure')]
        $DatumTree = $ExecutionContext.InvokeCommand.InvokeScript('$ConfigurationData.Datum'),

        [Parameter(
            ParameterSetName = 'UseMergeOptions'
        )]
        [Alias('SearchBehavior')]
        $options,

        [string[]]
        [Alias('SearchPaths')]
        $PathPrefixes = $DatumTree.__Definition.ResolutionPrecedence,

        [int]
        $MaxDepth = $(
                if($MxdDpth = $DatumTree.__Definition.default_lookup_options.MaxDepth) {
                    $MxdDpth
                }
                else {
                    -1
                })
    )

    # Manage lookup options:
    <#
    default_lookup_options Lookup_options options (argument) Behaviour
    Absent Absent Absent MostSpecific for ^.*
    Present Absent Absent default_lookup_options + most Specific if not ^.*
    Absent Present Absent lookup_options + Default to most Specific if not ^.*
    Absent Absent Present options + Default to Most Specific if not ^.*
    Present Present Absent Lookup_options + Default for ^.* if !Exists
    Present Absent Present options + Default for ^.* if !Exists
    Absent 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 |
    +========================+================+====================+============================================================+
    | Absent | Absent | Absent | MostSpecific for ^.* |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Present | Absent | Absent | default_lookup_options + most Specific if not ^.* |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Absent | Present | Absent | lookup_options + Default to most Specific if not ^.* |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Absent | Absent | Present | options + Default to Most Specific if not ^.* |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Present | Present | Absent | Lookup_options + Default for ^.* if !Exists |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Present | Absent | Present | options + Default for ^.* if !Exists |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Absent | Present | Present | options override lookup options + Most Specific if !Exists |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
    | Present | Present | Present | options override lookup options + default for ^.* |
    +------------------------+----------------+--------------------+------------------------------------------------------------+
 
    #>



    # https://docs.puppet.com/puppet/5.0/hiera_merging.html
    # Configure Merge Behaviour in the Datum structure (as per Puppet hiera)

    if( !$DatumTree.__Definition.default_lookup_options ) {
        $default_options = [ordered]@{
            '^.*' = @{
                strategy = 'MostSpecific'
            }
        }
        Write-Verbose "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
        }
        Write-Verbose "Found default options in Datum Tree of type $($default_options.Strategy)."
    }

    if( $lookup_options = $DatumTree.__Definition.lookup_options) {
        Write-Debug "Lookup options found."
    }
    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 (!$options) {
        $options = $lookup_options
    }

    # Add default strategy for ^.* if not present
    if(([string[]]$Options.keys) -notcontains '^.*') {
        $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 = [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 $SearchPrefix $PropertyPath
        Write-Verbose ''
        Write-Verbose "Searching: $CurrentSearch"
        #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

        Write-Debug "Depth: $depth; Merge Behavior: $($options|Convertto-Json|Out-String)"

        #Stop processing further path at first value in 'MostSpecific' mode (called 'first' in Puppet hiera)
        if ($DatumFound -and ($StartingMergeStrategy.Strategy -eq 'MostSpecific')) {
            return $DatumFound
        }
        elseif ( $DatumFound ) {

            if(!$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."
            return $MergeResult
        }
    }
    $MergeResult
}

function Resolve-DatumPath {
    [CmdletBinding()]
    param(
        [Alias('Variable')]
        $Node,

        [Alias('DatumStructure')]
        $DatumTree,

        [string[]]
        $PathStack,

        [System.Collections.ArrayList]
        $PathVariables
    )

    $currentNode = $DatumTree
    $PropertySeparator = '.' #[io.path]::DirectorySeparatorChar
    $index = -1
    Write-Debug "`t`t`t"

    foreach ($StackItem in $PathStack) {
        $index++
        $RelativePath = $PathStack[0..$index]
        Write-Debug "`t`t`tCurrent Path: `$Datum$PropertySeparator$($RelativePath -join $PropertySeparator)"
        $RemainingStack = $PathStack[$index..($PathStack.Count-1)]
        Write-Debug "`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))"
            Write-Output $CurrentNode
        }

    }
}

function Test-ProtectedDatumFilter {
    Param(
        [Parameter(
            ValueFromPipeline
        )]
        $InputObject
    )

    $InputObject -is [string] -and $InputObject.Trim() -match "^\[ENC=[\w\W]*\]$"
}

function Test-TestHandlerFilter {
    Param(
        [Parameter(
            ValueFromPipeline
        )]
        $inputObject
    )

    $InputObject -is [string] -and $InputObject -match "^\[TEST=[\w\W]*\]$"
}

#Requires -Modules ProtectedData

function Unprotect-Datum {
    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        # Serialized Protected Data represented on Base64 encoding
        [Parameter(
             Mandatory
            ,Position=0
            ,ValueFromPipeline
            ,ValueFromPipelineByPropertyName
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Base64Data,

        # By Password only for development / Test purposes
        [Parameter(
            ParameterSetName='ByPassword'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [System.Security.SecureString]
        $Password,

        # Specify the Certificate to be used by ProtectedData
        [Parameter(
            ParameterSetName='ByCertificate'
            ,Mandatory
            ,Position=1
            ,ValueFromPipelineByPropertyName
        )]
        [String]
        $Certificate,

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Header = '[ENC=',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [String]
        $Footer = ']',

        # Number of columns before inserting newline in chunk
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [Switch]
        $NoEncapsulation
    )

    begin {
    }

    process {
        if (!$NoEncapsulation) {
            Write-Verbose "Removing $header DATA $footer "
            $Base64Data = $Base64Data -replace "^$([regex]::Escape($Header))" -replace "$([regex]::Escape($Footer))$"
        }

        Write-Verbose "Deserializing the Object from Base64"
        $bytes = [System.Convert]::FromBase64String($Base64Data)
        $xml = [System.Text.Encoding]::UTF8.GetString($bytes)
        $obj = [System.Management.Automation.PSSerializer]::Deserialize($xml)
        $UnprotectDataParams = @{
            InputObject = $obj
        }
        Write-verbose "Calling Unprotect-Data $($PSCmdlet.ParameterSetName)"
        Switch ($PSCmdlet.ParameterSetName) {
            'ByCertificae' { $UnprotectDataParams.Add('Certificate',$Certificate)}
            'ByPassword'   { $UnprotectDataParams.Add('Password',$Password)      }
        }
        Unprotect-Data @UnprotectDataParams
    }

}