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 |