YamlObjectModel.psm1

using namespace System.Collections
using namespace System.Collections.Specialized
using namespace YamlDotNet.Core
using namespace YamlDotNet.Serialization
using namespace YamlDotNet.Core.Events
using namespace System.Collections.Generic
#Region './Classes/0.YOMApiDispatcher.ps1' 0
#using namespace System.Collections
#using namespace System.Collections.Specialized

class YOMApiDispatcher
{
    [string] $ApiVersion
    [string] $Kind
    [string] $Spec
    [OrderedDictionary] $Metadata = [ordered]@{}

    static [bool] IsDefinition([object] $Object) # Testing any object whether it's a definition
    {
        if ($Object -is [IDictionary] -and $Object.Contains('kind'))
        {
            return $true
        }
        else
        {
            return $false
        }
    }

    static [Object] DispatchSpec([string] $DefaultType, [IDictionary] $Definition)
    {
        if (-not $Definition.Contains('kind'))
        {
            Write-Debug "Dispatching spec as $DefaultType."
            return [YOMApiDispatcher]::DispatchSpec(
                [ordered]@{
                    kind = $DefaultType
                    spec = $Definition
                }
            )
        }
        else
        {
            Write-Debug "Definition defines kind, dispatching."
            return [YOMApiDispatcher]::DispatchSpec($Definition)
        }
    }

    static [Object] DispatchSpec([IDictionary] $Definition)
    {
        $moduleString = ''
        $returnCode = ''
        $action = ''

        if ($Definition.Kind -match '\\')
        {
            $moduleName, $action = $Definition.Kind.Split('\', 2)
            Write-Debug -Message "Module is '$moduleName'"
            if ($action -match '\-')
            {
                $moduleString = "Import-Module $moduleName"
            }
            else
            {
                $moduleString = "using module $moduleName"
            }
        }
        else
        {
            $action = $Definition.Kind
        }

        if ($action -match '\-')
        {
            # Function
            $functionName = $action
            Write-Debug -Message "Calling funcion $functionName"
            $returnCode = "`$params = `$Args[0]`r`n ,($functionName @params)"
        }
        elseif ($action -match '::')
        {
            # Static Method [class]::Method($spec)
            $className, $StaticMethod = $action.Split('::', 2)
            $StaticMethod = $StaticMethod.Trim('\(\):')
            $className = $className.Trim('\[\]')
            Write-Debug -Message "Calling static method '[$className]::$StaticMethod(`$spec)'"
            $returnCode = "return [$className]::$StaticMethod(`$args[0])"
        }
        else
        {
            # [Class]::New()
            $className = $action
            Write-Debug -Message ('Creating new [{0}]' -f $className)
            $returnCode = "return [$className]::new(`$args[0])"
        }

        $specObject = $Definition.spec
        $script = "$moduleString`r`n$returnCode"
        Write-Debug -Message "ScriptBlock = {`r`n$script`r`n}"
        $createdObject = [scriptblock]::Create($script).Invoke($specObject)[0]
        if ($createdObject.PSobject.Properties.Name -contains 'kind')
        {
            #TODO: Make sure the file metadata is also available here
            $createdObject.Kind = $Definition.Kind
        }

        return $createdObject
    }
}
#EndRegion './Classes/0.YOMApiDispatcher.ps1' 103
#Region './Classes/1.YOMBase.ps1' 0
#using namespace YamlDotNet.Core
#using namespace YamlDotNet.Serialization
#using namespace YamlDotNet.Core.Events
#using namespace System.Collections
#using namespace System.Collections.Generic
#using namespace System.Collections.Specialized

class YOMBase : IYamlConvertible
{
    [YamlIgnoreAttribute()]
    hidden [string] $kind
    [YamlIgnoreAttribute()]
    hidden [OrderedDictionary] $spec

    YOMBase()
    {
        # empty ctor
    }

    YOMBase([IDictionary]$RawSpec)
    {
        $this.ResolveSpec($RawSpec)
    }

    [void] Read([IParser] $Parser, [Type] $Type, [ObjectDeserializer] $NestedObjectDeserializer)
    {
        # $consumeGenericMethod = [IParser].GetMethod("TryConsume")
        # $closedConsumeGenericMethod = $consumeGenericMethod.MakeGenericMethod([SequenceStart])
        # $closedConsumeGenericMethod.Invoke()

        # while ($Parser.TryConsume<Scalar>(out var $key))
        # {
        # if ($key.Value == "config_two")
        # {
        # var config = deserializer.Deserialize<ConfigTwo>(parser);
        # Console.WriteLine(config.Random);
        # }
        # else
        # {
        # parser.SkipThisAndNestedEvents();
        # Console.WriteLine($"Skipped {key.Value}");
        # }
        # }
        # $scalar = $Parser.Allow()
        # if ($null -ne $scalar)
        # {
        # $this.Test = $scalar.Value
        # $this.Prod = $scalar.Value
        # }
        # else
        # {
        # # var values = (SettingsBase)nestedObjectDeserializer(typeof(SettingsBase));
        # # this.Test = values.Test;
        # # this.Prod = values.Prod;
        # }
    }

    [void] Write([IEmitter] $Emitter, [ObjectSerializer] $NestedObjectSerializer)
    {
        $outerObject = [ordered]@{
            kind = $this.GetType().ToString() # Problem here is that we don't know which module it's coming from...
            spec = [ordered]@{}
        }

        $this.PSObject.Properties.Where({
            $_.Name -in $this.GetType().GetProperties().Where{$_.CustomAttributes.AttributeType -ne [YamlDotNet.Serialization.YamlIgnoreAttribute]}.name -and
            $true -eq $_.IsSettable}).Foreach{
            $outerObject.spec.Add($_.Name,$_.Value)
        }

        $NestedObjectSerializer.Invoke($outerObject)
    }

    hidden [void] ResolveSpec([string] $kind, [IDictionary] $RawSpec)
    {
        $this.Kind = $RawSpec.kind
        $this.ResolveSpec($RawSpec)
    }

    hidden [void] ResolveSpec([IDictionary] $RawSpec)
    {
        if (-not [string]::IsNullOrEmpty($RawSpec.kind))
        {
            $this.ResolveSpec($RawSpec.kind,$RawSpec.Spec)
        }
        else
        {
            if (-not [string]::IsNullOrEmpty($this.kind))
            {
                $this.kind = $RawSpec.kind
            }

            $this.Spec = [Ordered]@{}

            foreach ($keyInSpec in $RawSpec.Keys)
            {
                Write-Debug -Message "Testing value of [$keyInSpec] for object definition..."
                $ValueForSpec = if ([YOMApiDispatcher]::IsDefinition($RawSpec.($keyInSpec)))
                {
                    # value is a nested object definition
                    Write-Debug -Message "Resolving value as an object."
                    [YOMApiDispatcher]::DispatchSpec($RawSpec.($keyInSpec))
                }
                else #TODO: make sure you have an elseif() when the object is a 'shorthand' of an object (handler or object)
                {
                    # Value is not a hash with kind, return as-is
                    Write-Debug -Message "The Value is --->$($RawSpec.($keyInSpec))"
                    $RawSpec.($keyInSpec)
                }

                $this.Spec.Add($keyInSpec,$ValueForSpec)
                if ($this.PSObject.Properties.Item($keyInSpec).issettable)
                {
                    $this.($keyInSpec) = $RawSpec.($keyInSpec)
                }
            }

        }
    }

    [string] ToJSON()
    {
        return ($this | ConvertTo-Yaml -Options EmitDefaults,JsonCompatible)
    }

    [string] ToYaml()
    {
        return ($this | ConvertTo-Yaml -Options EmitDefaults)
    }

    [string] ToString()
    {
        return $this.ToYaml()
    }
}
#EndRegion './Classes/1.YOMBase.ps1' 136
#Region './Classes/2.YOMSaveableBase.ps1' 0
#using namespace YamlDotNet.Core
#using namespace YamlDotNet.Serialization

class YOMSaveableBase : YOMBase
{
    # SavedAtPath
    [YamlIgnoreAttribute()]
    [string] $SavedAtPath

    # Constructor for Empty object
    YOMSaveableBase()
    {
        # Default Ctor
    }

    # Constructor For IDictionary (Hashtable, OrderedDictionary...)
    YOMSaveableBase([IDictionary]$Definition)
    {
        $this.ResolveSpec($Definition)
    }

    YOMSaveableBase([string] $Path)
    {
        $FilePath = Get-YOMAbsolutePath -Path $Path
        Write-Debug -Message "Loading settings from Path '$FilePath'."
        if (-not (Test-Path -Path $FilePath))
        {
            throw ('Error loading file ''{0}''' -f $FilePath)
        }

        $this.LoadFromFile($FilePath)
        $this.SavedAtPath = $FilePath
    }

    [void] LoadFromFile([string] $Path)
    {
        Write-Debug -Message "Loading Properties from '$Path'."
        $FilePath = Get-YOMAbsolutePath -Path $Path
        $Definition = ConvertFrom-Yaml -Ordered -Yaml (Get-Content -Raw -Path $FilePath)
        $this.ResolveSpec($Definition)
    }

    [void] Reload()
    {
        if ([string]::IsNullOrEmpty($this.SavedAtPath))
        {
            throw 'Cannot Reload when Definition file is not set. Try using the method .LoadFromFile([string] $Path) instead.'
            return
        }

        $this.LoadFromFile($this.SavedAtPath)
    }

    [void] Save()
    {
        # Save the Settings to file
        if ([string]::IsNullOrEmpty($this.SavedAtPath))
        {
            throw 'No file path is configured on the object to Save to. Please use the .SaveTo([string]$Path) method instead.'
        }
        else
        {
            $this.SaveTo($this.SavedAtPath)
        }
    }

    [void] SaveTo([string] $Path)
    {
        Write-Debug -Message "Saving the Definition to '$Path'."
        # Save the Configuration to a path (override if exists)
        $this.ToYaml() | Set-Content -Path $Path -Force
        $this.SavedAtPath = $Path
    }
}
#EndRegion './Classes/2.YOMSaveableBase.ps1' 75
#Region './Private/Get-YOMAbsolutePath.ps1' 0

<#
    .SYNOPSIS
        Gets the absolute value of a path, that can be relative to another folder
        or the current Working Directory `$PWD` or Drive.

    .DESCRIPTION
        This function will resolve the Absolute value of a path, whether it's
        potentially relative to another path, relative to the current working
        directory, or it's provided with an absolute Path.

        The Path does not need to exist, but the command will use the right
        [System.Io.Path]::DirectorySeparatorChar for the OS, and adjust the
        `..` and `.` of a path by removing parts of a path when needed.

    .PARAMETER Path
        Relative or Absolute Path to resolve, can also be $null/Empty and will
        return the RelativeTo absolute path.
        It can be Absolute but relative to the current drive: i.e. `/Windows`
        would resolve to `C:\Windows` on most Windows systems.

    .PARAMETER RelativeTo
        Path to prepend to $Path if $Path is not Absolute.
        If $RelativeTo is not absolute either, it will first be resolved
        using [System.Io.Path]::GetFullPath($RelativeTo) before
        being pre-pended to $Path.

    .EXAMPLE
        Get-YOMAbsolutePath -Path '/src' -RelativeTo 'C:\Windows'
        # C:\src

    .EXAMPLE
        Get-YOMAbsolutePath -Path 'MySubFolder' -RelativeTo '/src'
        # C:\src\MySubFolder

    .NOTES
        When the root drive is omitted on Windows, the path is not considered absolute.
        `Split-Path -IsAbsolute -Path '/src/`
        # $false
#>

function Get-YOMAbsolutePath
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter()]
        [AllowNull()]
        [System.String]
        $Path,

        [Parameter()]
        [System.String]
        $RelativeTo
    )

    if (-not [System.Io.Path]::IsPathRooted($RelativeTo))
    {
        # If the path is not rooted it's a relative path
        $RelativeTo = Join-Path -Path $PWD.ProviderPath -ChildPath $RelativeTo
    }
    elseif (-not (Split-Path -IsAbsolute -Path $RelativeTo) -and [System.Io.Path]::IsPathRooted($RelativeTo))
    {
        # If the path is not Absolute but is rooted, it's starts with / or \ on Windows.
        # Add the Current PSDrive root
        $CurrentDriveRoot = $pwd.drive.root
        $RelativeTo = Join-Path -Path $CurrentDriveRoot -ChildPath $RelativeTo
    }

    if ($PSVersionTable.PSVersion.Major -ge 7)
    {
        # This behave differently in 5.1 where * are forbidden. :(
        $RelativeTo = [System.io.Path]::GetFullPath($RelativeTo)
    }

    if (-not [System.Io.Path]::IsPathRooted($Path))
    {
        # If the path is not rooted it's a relative path (relative to $RelativeTo)
        $Path = Join-Path -Path $RelativeTo -ChildPath $Path
    }
    elseif (-not (Split-Path -IsAbsolute -Path $Path) -and [System.Io.Path]::IsPathRooted($Path))
    {
        # If the path is not Absolute but is rooted, it's starts with / or \ on Windows.
        # Add the Current PSDrive root
        $CurrentDriveRoot = $pwd.drive.root
        $Path = Join-Path -Path $CurrentDriveRoot -ChildPath $Path
    }
    # Else The Path is Absolute

    if ($PSVersionTable.PSVersion.Major -ge 7)
    {
        # This behave differently in 5.1 where * are forbidden. :(
        $Path = [System.io.Path]::GetFullPath($Path)
    }

    return $Path
}
#EndRegion './Private/Get-YOMAbsolutePath.ps1' 98
#Region './Public/Get-YOMObject.ps1' 0
function Get-YOMObject
{
    [CmdletBinding(DefaultParameterSetName= 'ByPath')]
    param (
        [Parameter(ParameterSetName = 'ByPath', Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Path,

        [Parameter(ParameterSetName = 'ByDictionary', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Collections.IDictionary[]]
        $Definition,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $DefaultType
    )

    begin
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByPath')
        {
            $Definition = foreach ($pathItem in $Path)
            {
                $files = if (Test-Path -Path $pathItem -PathType Container)
                {
                    #TODO: Handle new defaults on subdirectories if defined
                    (Get-ChildItem -Path $pathItem -File -Include *.yml -Recurse).FullName
                }
                else
                {
                    Get-YOMAbsolutePath -Path $pathItem
                }

                $files.foreach({
                    $fileItem = $_
                    #TODO: Each file is a container representing objects
                    (Get-Content -Raw -Path $_ |
                      ConvertFrom-Yaml -AllDocuments -Ordered).Foreach{
                        if ([string]::IsNullOrEmpty($_.kind))
                        {
                            $_['SavedAtPath'] = $fileItem
                        }
                        else
                        {
                            $_['spec']['SavedAtPath'] = $fileItem
                        }

                        $_ #returning definition dans $Definition
                      }
                })
            }
        }
    }

    process
    {
        foreach ($objectDefinition in $Definition)
        {
            if ($DefaultType)
            {
                Write-Debug -Message "Trying to build the object [DefaultType: $DefaultType].`r`n$($objectDefinition)"
                [YOMApiDispatcher]::DispatchSpec($DefaultType, $objectDefinition)
            }
            else
            {
                Write-Debug -Message "Trying to build the object:`r`n $($objectDefinition | ConvertTo-Yaml -Options EmitDefaults)"
                [YOMApiDispatcher]::DispatchSpec($objectDefinition)
            }
        }
    }
}
#EndRegion './Public/Get-YOMObject.ps1' 75