private/_PSTSYaml.ps1

$includeValues = [ordered]@{ }


function _fromYAML
{
    [CmdletBinding()]
    param (
        [parameter(Position = 0, Mandatory = $false, ValueFromPipeline = $true)] [string] $YamlString,
        [parameter(Position = 1, Mandatory = $false, ValueFromPipeline = $false)][string] $Path,
        [switch]$representational
    )


        try {

            If($Path) {

                $streamReader = [System.IO.File]::OpenText($Path)
            }
            Else {

                $streamReader = new-object System.IO.StringReader([string]$yamlString)
            }

            $yamlStream = New-Object YamlDotNet.RepresentationModel.YamlStream
            $yamlStream.Load([System.IO.TextReader]$streamReader)

            $root = $yamlStream.Documents[0]

            if ($representational) {
                return ,$root.RootNode
            }

            [System.Collections.Specialized.OrderedDictionary]$tree = _process ($yamlStream.Documents[0])

            return $tree

        }
        Catch {
            Throw $_
        }

        Finally {

            if ($null -ne $streamReader.Basestream) {
                $streamReader.Close()

                if ( $representational -eq $false) {
                    $includeValues.Clear()
                }
            }
        }
}


function _parseScalar {

    [OutputType([System.String],[System.Int32],[System.Int64],[System.Decimal],[System.Single],[System.Double], [System.DateTime])]
    [CmdletBinding()]
    param (
        [YamlDotNet.RepresentationModel.YamlScalarNode]$node
    )

    $value = "$($node)"

    #manage yes/no |on/off values
    $value = switch -Regex ($value) {

            '(?i)\A(?:on|yes)\z' {
                'true'
                break
            }

            '(?i)\A(?:off|no)\z' {
                'false'
                break
            }

            default { $value }
    }

    $out = $null
    if ([bool]::TryParse($value, [ref]$out)) { return $out}

    if ( $value -as [int] -or $value -eq "0" ) { return [int]$value }
    if ( $value -as [long] ) { return [long]$value }
    if ( $value -as [decimal] ) { return [decimal]$value }
    if ( $value -as [single] ) { return [single]$value }
    if ( $value -as [double] ) { return [double]$value }
    #if ( $value -as [DateTime] ) { return [DateTime]$value }


    return [string]$value


}

function _getFromKey() {
    [CmdletBinding()]
    param ( [object[]]$lookup, [string]$key )

    $data = $null
    Foreach ($n in $lookup) {

        if ($n.Key -eq $key) {
            $data = $n.Value
            break
        }
        elseif ($n.GetType().Name -eq "YamlSequenceNode") {
            return _getFromKey -lookup $n -key $key
        }
    }

    return ,$data
}


function _parseFunction {

    [CmdletBinding()]
    param (
        [object]$node,
        [object]$scope = $null )

    if ($node.GetType().Name -ne "YamlSequenceNode") {
        return $node
    }

    $typed = [YamlDotNet.RepresentationModel.YamlSequenceNode]$node

    function isNumeric([System.Object] $obj) {
        return $obj -is [byte]  -or $obj -is [int16]  -or $obj -is [Int32]  -or $obj -is [int64]  `
           -or $obj -is [sbyte] -or $obj -is [uint16] -or $obj -is [uint32] -or $obj -is [uint64] `
           -or $obj -is [float] -or $obj -is [double] -or $obj -is [decimal]
    }

    switch ($typed.Tag) {
        "!add" {

            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $tmp = 0
            foreach($obj in $typed.Children) {
                $val = _process $obj $scope
                if(!(isNumeric($val))) { Throw "invalid value : $($val)" }
                $tmp = $tmp + $val
            }

            $ret.Value = $tmp
        }

        "!mul" {

            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $tmp = 1
            foreach($obj in $typed.Children) {
                $val = _process $obj $scope
                if(!(isNumeric($val))) { Throw "invalid value : $($val)" }
                $tmp = $tmp * $val
            }

            $ret.Value = $tmp
        }

        "!count" {
            $val = _process $($typed | Select-Object -First 1) $scope
            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $ret.Value = $val.Count
        }

        "!tolower" {
            #if($typed.Value.Count -ne 1) { Throw "Too many strings to lower" }
            $val = _process ($typed | Select-Object -First 1) $scope
            if(!($val -is [string])) { Throw "$($val) is not a string" }
            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $ret.Value = $val.ToLower()
        }
        "!toupper" {
            #if($typed.Value.Count -ne 1) { Throw "Too many strings to upper" }
            $val = _process ($typed | Select-Object -First 1) $scope
            if(!($val -is [string])) { Throw "$($val) is not a string" }
            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $ret.Value = $val.ToUpper()
        }
        "!include" {

            $file = _process $($typed | Select-Object -First 1) $scope
            $key =  _process $($typed | Select-Object -skip 1 -First 1) $scope
            $values= _fromYAML -Path $file -representational
            $Includevalues.Add($key, $values)

            break
        }

        "!ruleset" {
            $value = _process $($typed | Select-Object -First 1) $scope
            $rulesetNode = $($typed | Select-Object -First 1 -skip 1)
            $ruleset = _process $rulesetNode $scope
             $match = [regex]::IsMatch($value, $ruleset)

             if ($match -eq $true) {
                throw [exception]::new("ruleset matched a forbidden string on $value .")

             }
            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()
            $ret.Value = $value
        }

        "!get" {

          $key = _process $($typed | Select-Object -First 1) $scope
          $data = _process  $($typed | Select-Object -skip 1 -First 1) $scope
          if($data.GetType().Name -eq 'OrderedDictionary') {
            $list = $data.Keys
          }
          else {
            $list = $data.Split(".")
          }
          $toleration = (_process $($typed | Select-Object -skip 2 -First 1 ) $scope)
          $rulesetNode = $($typed | Select-Object -skip 3 -First 1 )

          if ($null -ne $rulesetNode) {
            $ruleset = _process $rulesetNode $scope
          }

          $values = $Includevalues.($key)

          $tmp = $null

          $lookup = $values

          foreach ($item in $list) {

              if ($item -eq "*") {
                $tmp = $lookup
                break
              }


              $tmp = _getFromKey -lookup $lookup -key $item

              if ($null -ne $tmp) {
                $lookup = ($lookup | Where-Object { $null -eq (Compare-Object $_.Value $tmp) }).Value
              }
          }

          #if ($tmp -eq $null) {
          # return $null
          #}

          if ($null -ne $ruleset -and $null -ne $tmp -and $tmp.GetType().Name -eq "YamlScalarNode") {
             $match = [regex]::IsMatch($tmp.Value, $ruleset)

             if ($match -eq $true) {
                throw [exception]::new("ruleset matched a forbidden string on $($tmp.Value) .")

             }
          }

          if ($null -eq $tmp -and $toleration -eq $false) {
            throw [exception]::new("Null value returned for $data with no toleration set")
          }

          return ,$tmp

        }

        "!concat" {

            $ret = [YamlDotNet.RepresentationModel.YamlScalarNode]::new()

            $typed | ForEach-Object -process {
                $current =  _parseFunction $_ $scope

                $ret.Value = "$($ret.Value)$($current.value)"
            }

            break
        }

        "!format" {
            $ret = @()
            $toFormat = [YamlDotNet.RepresentationModel.YamlScalarNode]::new($($typed | Select-Object -First 1))

            $formatParams = @()

            $($typed | Select-Object -skip 1) | ForEach-Object -process {

                $data = _parseFunction $_ $scope
                $cast = $data -as [YamlDotNet.RepresentationModel.YamlNode]

                $formatParams += $cast.value

            }

            $toFormat.Value = $toFormat.Value -f $formatParams
            return ,$toFormat

            break
        }

        default {
            $ret = $typed
            break
        }
    }

    return ,$ret
}

function _foreach {

    [CmdletBinding()]
    param (
    $dataBlock,
    $collection,
    $iterator,
    $scope )

    $new = @()

    $collection | ForEach-Object {
       $block = [YamlDotNet.RepresentationModel.YamlSequenceNode]::new()
       $scope[$iterator] = $_
       $block = _process -node $dataBlock -obj $scope
       $new += @($block)

    }

    return ,$new
}

function _process {

    [CmdletBinding()]
    param (
        $node,
        $obj=$null
    )

    $nodeType = $node.GetType().Name
    Write-Verbose "$nodeType = $($node)"

    switch ($nodeType)
    {
        "OrderedDictionary" {
            return $node
        }
        "YamlDocument" {

            $typed = [YamlDotNet.RepresentationModel.YamlDocument]$node

            return _process $typed.RootNode $obj
        }

        "YamlMappingNode" {

            $typed = [YamlDotNet.RepresentationModel.YamlMappingNode]$node

            $ret = [ordered]@{ }

            foreach ($item in $typed) {

                #check if we match a directive and process accordingly
                $match = [regex]::Match($item.Key.Value, "{{([a-zA-z]*?)}}")

                if ($match.Success) {

                     $parsed = _parseDirectives `
                    -directive $item `
                    -node ($typed.Children.GetEnumerator() | Where-Object { $_.Key.Value -eq "{{do}}" }) `
                    -scope $obj

                    #$parsed = _parseDirectives `
                    #-directive $item `
                    #-node ($typed | Select-Object -skip 1 -First 1) `
                    #-scope $obj


                    return ,$parsed
                    break

                }
                else {
                    $data = _process $item.Value $obj

                    if ($null -ne $data) {
                        $ret.($item.Key.Value) = $data
                    }
                }
            }

            return ,$ret
        }

        "YamlSequenceNode" {

            $parsed = _parseFunction $node $obj

            if ($null -eq $parsed) {
                return $null
            }
            if ($parsed.GetType().Name -ne "YamlSequenceNode") {

                $ret = _process $parsed $obj
            }
            else
            {
               $ret = @()

                foreach($i in $parsed) {

                    $item = _process $i $obj

                    if ($null -ne $item -and $item.count -ne 0) {
                        $ret+=$item
                    }
                }
            }


            return ,$ret
        }

        "YamlScalarNode" {

            $matches = [regex]::Matches($node.Value,"%%[^\s%%=\/!@#$%^&*(),?`":{}|<>]+")

            if ($matches.Count -eq 0) {
                return _parseScalar $node
            }


            $tmp = [YamlDotNet.RepresentationModel.YamlScalarNode]::new($node.Value)


            foreach ($match in $matches) {

                $target = $match.Value.Substring(2)
                $resolveArray= $target.split(".")
                $iterator = $resolveArray[0]
                $lookupScope = $obj[$iterator]

                if ($resolveArray.Count -eq 1 -and $resolveArray[0] -eq $iterator) {
                    if ($lookupScope.GetType().Name -eq "YamlScalarNode") {
                            $tmp.Value = $tmp.Value -replace $match.Value, $lookupScope.Value



                            break
                        }
                        else {
                            $tmp = $lookupScope.Value
                            break
                        }

                }

                foreach ($k in $lookupScope) {
                    $data = $k.($resolveArray[1])

                    foreach ($i in ($resolveArray | Select -Skip 2)) {
                        $data = $data.($i)
                    }

                    if ($null -ne $data) {
                    #if ($resolveArray[1] -eq $k.Key.Value) {

                        if (($data.GetType().Name -ne "OrderedDictionary") -and ($data.GetType().Name -ne "Object[]")) {
                        #if ($k.Value.GetType().Name -eq "YamlScalarNode") {
                            $tmp.Value = $tmp.Value -replace $match.Value, $data
                            # $tmp.Value = $tmp.Value -replace $match.Value, $k.Value.Value

                            break
                        }
                        else {
                            $tmp = $data #$k.Value
                            break
                        }
                    }
                }

                if ($tmp.ToString() -eq $node.Value) {
                    return $null
                }
            }


            $override = ($includeValues.Values | Where-Object { $_.Key -eq $tmp -or $_.Key -eq $tmp.value }).Value.Value
            #$override = ($includeValues.Values | Where-Object { $_.Key -eq $tmp.Value }).Value.Value

            if ($override) {
                $tmp = $override
                return _process $tmp $obj
            }
            else
            {
                return _process $tmp $obj
            }
            #if ($override) {
            # $tmp.Value = $override
            #}

            #return _process $tmp $obj
        }

        default {
            return $node
        }
    }
}


Function _parseDirectives {

    [CmdletBinding()]
    param (

        [object]$directive,
        [object]$node,
        [object]$scope = $null

    )

    if ($null -eq $scope) {
        $scope = [ordered]@{}
    }


    $func = $directive.Key.Value

    if ($func.Contains("{{export}}")) {

        $exportKey = _parseFunction ($directive.Value | Select-Object -First 1) $scope
        $exportRaw = _parseFunction ($directive.Value | Select-Object -First 1 -skip 1) $scope

        $isAppend = _process ($directive.Value | Select-Object -First 1 -skip 2) $scope

        $exportValue = _process -node $exportRaw -obj $scope

        if ( $null -eq $Includevalues.($exportKey.Value)) {
            $Includevalues.Add($exportKey.Value, $exportValue)
        }
        elseif ($isAppend -eq $true) {

            $Includevalues.($exportKey.Value) = "{0} {1}" -f $Includevalues.($exportKey.Value), $exportValue
        }
        elseif ($isAppend -eq $false) {

            $Includevalues.($exportKey.Value) = $exportValue
        }

        return $null
    }

    if ($func.Contains("{{foreach}}")) {
         $collection = _parseFunction ($directive.Value | Select-Object -First 1) $scope

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

        # if ($collection.GetType().Name -eq "YamlScalarNode") {
            #this must return a valid yaml. it will fails otherwise, which is the case calling process
            #because it will returns an ordereddictionary which is the input !
            $collection = _process -node $collection -obj $scope
         # }

         $iterator = _parseFunction ($directive.Value | Select-Object -First 1 -skip 1) $scope

         $ret = _foreach -collection $collection  $node.Value $iterator.Value  $scope
         return ,$ret
    }

     if ($func.Contains("{{if}}")) {

         $right = _process $(_parseFunction ($directive.Value | Select-Object -First 1) $scope) $scope
         $operand = _process $(_parseFunction ($directive.Value | Select-Object -First 1 -Skip 1) $scope) $scope
         $left = _process $(_parseFunction ($directive.Value | Select-Object -First 1 -Skip 2) $scope) $scope

         if ($left -ne $null -and $left.ToString() -eq "null") {
            $left = $null
         }

         if ($left -ne $null -and $right.ToString() -eq "null") {
            $right = $null
         }


         if ($null -ne $right -or $null -ne $left) {
            $op = Invoke-Expression "return `"$right`" $operand `"$left`""

            if ($op -eq $true) {
                $ret = _process $node.Value $scope
            }
         }
         return ,$ret
     }

     #skip, although it is not mandatory it keeps the eyes open on the chart
     #if ($func.Contains("{{end}}")) {

     #}


     return $ret
}