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' -1

# 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 ($Definition.Contains('kind'))
        {
            Write-Debug 'Definition defines kind, dispatching.'
            return [YOMApiDispatcher]::DispatchSpec($Definition)
        }
        elseif (
            $Definition.Contains('spec') -and
            ($Definition.Contains('apiVersion') -or $Definition.Contains('metadata'))
        )
        {
            Write-Debug "Dispatching typed short envelope as $DefaultType."
            $typedDefinition = [ordered]@{
                apiVersion = if ($Definition.Contains('apiVersion'))
                {
                    $Definition['apiVersion']
                }
                else
                {
                    ''
                }
                kind = $DefaultType
                metadata = if ($Definition.Contains('metadata'))
                {
                    $Definition['metadata']
                }
                else
                {
                    [ordered]@{}
                }
                spec = $Definition['spec']
            }

            return [YOMApiDispatcher]::DispatchSpec($typedDefinition)
        }
        else
        {
            Write-Debug "Dispatching spec as $DefaultType."
            return [YOMApiDispatcher]::DispatchSpec(
                [ordered]@{
                    kind = $DefaultType
                    spec = $Definition
                }
            )
        }
    }

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

        if (-not [YOMApiDispatcher]::IsDefinition($Definition))
        {
            throw 'The Definition does not infer the object type to create from those properties. Please define it under the ''kind'' key.'
        }
        elseif (-not $Definition.Contains('spec') -or $Definition['spec'] -isnot [IDictionary])
        {
            throw 'The Definition must contain a dictionary under the ''spec'' key.'
        }
        elseif ($Definition.Kind -match '\\')
        {
            $moduleName, $action = $Definition.Kind.Split('\', 2)
            $moduleLoaded = Get-Module -Name $moduleName -ErrorAction SilentlyContinue
            if ($null -ne $moduleLoaded)
            {
                $moduleString = ('$m = Get-Module -Name ''{0}'' -ErrorAction ''Stop''{1}' -f $moduleName,"`r`n")
            }
            elseif ($action -match '\-')
            {
                $moduleString = "Import-Module $moduleName"
            }
            else
            {
                $moduleString = "using module $moduleName"
            }

            Write-Debug -Message ('Module import: {0}' -f $moduleString)
        }
        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)'"
            if ($null -ne $moduleLoaded)
            {
                $returnCode = "return (&`$m {return [$className]::$StaticMethod(`$args[0])} `$args[0])"
            }
            else
            {
                $returnCode = "return [$className]::$StaticMethod(`$args[0])"
            }
        }
        else
        {
            # [Class]::New()
            $className = $action
            Write-Debug -Message ('Creating new [{0}]' -f $className)
            if ($null -ne $moduleLoaded)
            {
                $returnCode = "return (&`$m {[$className]::new(`$args[0])} `$args[0])"
            }
            else
            {
                $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')
        {
            $createdObject.Kind = $Definition.Kind
        }

        if ($createdObject.PSobject.Properties.Name -contains 'ApiVersion')
        {
            $createdObject.ApiVersion = if ($Definition.Contains('apiVersion'))
            {
                [string] $Definition['apiVersion']
            }
            else
            {
                ''
            }
        }

        if ($createdObject.PSobject.Properties.Name -contains 'Metadata')
        {
            $createdObject.Metadata = [ordered]@{}
            if ($Definition.Contains('metadata') -and $null -ne $Definition['metadata'])
            {
                if ($Definition['metadata'] -isnot [IDictionary])
                {
                    throw 'The Definition metadata must be a dictionary.'
                }

                foreach ($metadataKey in $Definition['metadata'].Keys)
                {
                    $createdObject.Metadata[$metadataKey] = $Definition['metadata'][$metadataKey]
                }
            }
        }

        return $createdObject
    }
}
#EndRegion './Classes/0.YOMApiDispatcher.ps1' 189
#Region './Classes/1.YOMBase.ps1' -1

# 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()]
    [string] $ApiVersion = ''
    [YamlIgnoreAttribute()]
    [string] $Kind = ''
    [YamlIgnoreAttribute()]
    [OrderedDictionary] $Metadata = [ordered]@{}
    [YamlIgnoreAttribute()]
    hidden [OrderedDictionary] $Spec = [ordered]@{}

    YOMBase()
    {
        # empty ctor
    }

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

    [void] Read([IParser] $Parser, [Type] $Type, [ObjectDeserializer] $NestedObjectDeserializer)
    {
        # TODO
        # This is to parse Yaml to this object when we use an annotation registered with the parser
        # I don't think that's possible with PowerShell-Yaml yet.
    }

    [void] Write([IEmitter] $Emitter, [ObjectSerializer] $NestedObjectSerializer)
    {
        $outerObject = [ordered]@{}

        if (-not [string]::IsNullOrEmpty($this.ApiVersion))
        {
            $outerObject['apiVersion'] = $this.ApiVersion
        }

        $outerObject['kind'] = if ([string]::IsNullOrEmpty($this.Kind))
        {
            $this.GetType().ToString()
        }
        else
        {
            $this.Kind
        }

        if ($null -ne $this.Metadata -and $this.Metadata.Count -gt 0)
        {
            $outerObject['metadata'] = $this.Metadata
        }

        $outerObject['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 = $kind
        $this.ResolveSpecProperties($RawSpec)
    }

    hidden [void] ResolveSpec([IDictionary] $RawSpec)
    {
        if ($null -eq $RawSpec)
        {
            throw [System.ArgumentNullException]::new('RawSpec')
        }

        if ($RawSpec.Contains('kind'))
        {
            $this.Kind = [string] $RawSpec['kind']

            if ($RawSpec.Contains('apiVersion'))
            {
                $this.ApiVersion = [string] $RawSpec['apiVersion']
            }

            if ($RawSpec.Contains('metadata'))
            {
                $this.Metadata = [ordered]@{}
                if ($null -ne $RawSpec['metadata'])
                {
                    if ($RawSpec['metadata'] -isnot [IDictionary])
                    {
                        throw [System.ArgumentException]::new('metadata must be a dictionary.')
                    }

                    foreach ($metadataKey in $RawSpec['metadata'].Keys)
                    {
                        $this.Metadata[$metadataKey] = $RawSpec['metadata'][$metadataKey]
                    }
                }
            }

            if (-not $RawSpec.Contains('spec') -or $RawSpec['spec'] -isnot [IDictionary])
            {
                throw [System.ArgumentException]::new('spec must be a dictionary.')
            }

            $this.ResolveSpecProperties($RawSpec['spec'])
        }
        elseif (
            $RawSpec.Contains('spec') -and
            ($RawSpec.Contains('apiVersion') -or $RawSpec.Contains('metadata'))
        )
        {
            if ($RawSpec.Contains('apiVersion'))
            {
                $this.ApiVersion = [string] $RawSpec['apiVersion']
            }

            if ($RawSpec.Contains('metadata'))
            {
                $this.Metadata = [ordered]@{}
                if ($null -ne $RawSpec['metadata'])
                {
                    if ($RawSpec['metadata'] -isnot [IDictionary])
                    {
                        throw [System.ArgumentException]::new('metadata must be a dictionary.')
                    }

                    foreach ($metadataKey in $RawSpec['metadata'].Keys)
                    {
                        $this.Metadata[$metadataKey] = $RawSpec['metadata'][$metadataKey]
                    }
                }
            }

            if ($RawSpec['spec'] -isnot [IDictionary])
            {
                throw [System.ArgumentException]::new('spec must be a dictionary.')
            }

            $this.ResolveSpecProperties($RawSpec['spec'])
        }
        else
        {
            $this.ResolveSpecProperties($RawSpec)
        }
    }

    hidden [void] ResolveSpecProperties([IDictionary] $RawSpec)
    {
        $this.Spec = [ordered]@{}

        foreach ($keyInSpec in $RawSpec.Keys)
        {
            Write-Debug -Message "Testing value of [$keyInSpec] for object definition..."
            $ValueForSpec = if ([YOMApiDispatcher]::IsDefinition($RawSpec.($keyInSpec)))
            {
                Write-Debug -Message 'Resolving value as an object.'
                [YOMApiDispatcher]::DispatchSpec($RawSpec.($keyInSpec))
            }
            else
            {
                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' 199
#Region './Classes/2.YOMSaveableBase.ps1' -1

# 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' -1


<#
    .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' -1

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
    {
        $dispatchDefinition = {
            param
            (
                [Parameter()]
                [IDictionary]
                $ObjectDefinition,

                [Parameter()]
                [string]
                $SourcePath
            )

            $createdObject = 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)
            }

            if (
                -not [string]::IsNullOrEmpty($SourcePath) -and
                $createdObject.PSObject.Properties.Name -contains 'SavedAtPath'
            )
            {
                $createdObject.SavedAtPath = $SourcePath
            }

            return $createdObject
        }
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByPath')
        {
            foreach ($pathItem in $Path)
            {
                $files = if (Test-Path -Path $pathItem -PathType Container)
                {
                    (Get-ChildItem -Path $pathItem -File -Filter '*.yml' -Recurse).FullName
                }
                else
                {
                    Get-YOMAbsolutePath -Path $pathItem
                }

                foreach ($fileItem in $files)
                {
                    $objectDefinitions = Get-Content -Raw -Path $fileItem |
                        ConvertFrom-Yaml -AllDocuments -Ordered

                    foreach ($objectDefinition in $objectDefinitions)
                    {
                        & $dispatchDefinition $objectDefinition $fileItem
                    }
                }
            }
        }
        else
        {
            foreach ($objectDefinition in $Definition)
            {
                & $dispatchDefinition $objectDefinition ''
            }
        }
    }
}
#EndRegion './Public/Get-YOMObject.ps1' 94